From 1bae3bcad2beaff74274b8bbbde6e873a3b0b2f6 Mon Sep 17 00:00:00 2001 From: Andrey Buzin Date: Tue, 7 Apr 2026 10:41:28 -0700 Subject: [PATCH 1/6] Outline agents api and new runtime --- src/vercel_ai_sdk/agents3/__init__.py | 13 +++ src/vercel_ai_sdk/agents3/agent.py | 79 +++++++++++++++++ src/vercel_ai_sdk/agents3/runtime.py | 71 ++++++++++++++++ src/vercel_ai_sdk/agents3/tools.py | 118 ++++++++++++++++++++++++++ src/vercel_ai_sdk/models/__init__.py | 55 ++++++++++-- 5 files changed, 327 insertions(+), 9 deletions(-) create mode 100644 src/vercel_ai_sdk/agents3/__init__.py create mode 100644 src/vercel_ai_sdk/agents3/agent.py create mode 100644 src/vercel_ai_sdk/agents3/runtime.py create mode 100644 src/vercel_ai_sdk/agents3/tools.py diff --git a/src/vercel_ai_sdk/agents3/__init__.py b/src/vercel_ai_sdk/agents3/__init__.py new file mode 100644 index 00000000..7fdbbb96 --- /dev/null +++ b/src/vercel_ai_sdk/agents3/__init__.py @@ -0,0 +1,13 @@ +from .agent import Agent, Context, StreamResult, agent, stream +from .tools import Tool, ToolCall, tool + +__all__ = [ + "Agent", + "Context", + "StreamResult", + "Tool", + "ToolCall", + "agent", + "stream", + "tool", +] diff --git a/src/vercel_ai_sdk/agents3/agent.py b/src/vercel_ai_sdk/agents3/agent.py new file mode 100644 index 00000000..806b595c --- /dev/null +++ b/src/vercel_ai_sdk/agents3/agent.py @@ -0,0 +1,79 @@ +"""Agent, Context, StreamResult, and the stream() function.""" + +from __future__ import annotations + +import asyncio +from collections.abc import AsyncGenerator, AsyncIterable, Sequence +from typing import Any, Protocol + +import pydantic + +from .. import models, types +from . import runtime, tools as tools_ + + +class Context(pydantic.BaseModel): + """Everything that goes into the LLM""" + + messages: list[types.Message] + tools: list[ + tools_.Tool[..., Any] + ] # TODO should be something serializable like schema + + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + + +class LoopFn(Protocol): + def __call__(self, context: Context) -> AsyncGenerator[types.Message]: ... + + +async def _default_loop(context: Context) -> AsyncGenerator[types.Message]: + while True: + stream = models.stream(context.model, context.messages) + async for message in stream: + yield message + + async with asyncio.TaskGroup() as tg: + pass + # todo call tools + # yield tool messages + + +class Agent: + """Bag of configuration: model + tools + loop.""" + + def __init__( + self, + *, + tools: list[tools_.Tool[..., Any]] | None = None, + ) -> None: + self._tools: list[tools_.Tool[..., Any]] = tools or [] + self._loop_fn: LoopFn = _default_loop + + def loop(self, fn: LoopFn) -> LoopFn: + """Decorator: override the default loop function.""" + self._loop_fn = fn + return fn + + async def run( + self, model: models.Model, messages: list[types.Message] + ) -> AsyncGenerator[types.Message]: + """Run the agent loop, yielding messages to the consumer.""" + + # todo: validate messages, maybe inject standard system message + # todo: check tools, allow passing filtered list of tools + + context = Context(messages=messages, tools=self._tools) + + async for message in runtime.run(self._loop_fn(context)): + yield message + + +def agent( + *, + model: models.Model, + tools: list[Tool[..., Any]] | None = None, + system: str | None = None, +) -> Agent: + """Create an Agent.""" + return Agent(tools=tools) diff --git a/src/vercel_ai_sdk/agents3/runtime.py b/src/vercel_ai_sdk/agents3/runtime.py new file mode 100644 index 00000000..964430ca --- /dev/null +++ b/src/vercel_ai_sdk/agents3/runtime.py @@ -0,0 +1,71 @@ +"""Runtime: message sink that connects producer coroutines to the consumer.""" + +from __future__ import annotations + +import asyncio +import contextvars +from collections.abc import AsyncGenerator, AsyncIterable, Awaitable +from typing import Any + +from .. import types + + +class Runtime: + """Central message queue. Producers put messages, run() yields them.""" + + class _Sentinel: + pass + + _SENTINEL = _Sentinel() + + def __init__(self) -> None: + self._message_queue: asyncio.Queue[types.Message | Runtime._Sentinel] = ( + asyncio.Queue() + ) + + async def put_message(self, message: types.Message) -> None: + await self._message_queue.put(message) + + async def signal_done(self) -> None: + await self._message_queue.put(self._SENTINEL) + + +_runtime: contextvars.ContextVar[Runtime] = contextvars.ContextVar("runtime") + + +def get_runtime() -> Runtime: + """Return the active Runtime. Raises LookupError outside of run().""" + return _runtime.get() + + +async def _stop_when_done(runtime: Runtime, task: Awaitable[None]) -> None: + try: + await task + finally: + await runtime.signal_done() + + +async def run( + source: AsyncIterable[types.Message], +) -> AsyncGenerator[types.Message]: + """Run *source* and yield every message that gets put into the Runtime queue.""" + + runtime = Runtime() + token = _runtime.set(runtime) + + async def _drain() -> None: + async for message in source: + await runtime.put_message(message) + + try: + async with asyncio.TaskGroup() as tg: + tg.create_task(_stop_when_done(runtime, _drain())) + + while True: + item = await runtime._message_queue.get() + if isinstance(item, Runtime._Sentinel): + return + yield item + + finally: + _runtime.reset(token) diff --git a/src/vercel_ai_sdk/agents3/tools.py b/src/vercel_ai_sdk/agents3/tools.py new file mode 100644 index 00000000..63d30328 --- /dev/null +++ b/src/vercel_ai_sdk/agents3/tools.py @@ -0,0 +1,118 @@ +"""Tool decorator, Tool class, and ToolCall callable wrapper.""" + +from __future__ import annotations + +import inspect +import json +from collections.abc import AsyncGenerator, Awaitable, Callable +from typing import Any, get_type_hints, overload + +import pydantic + +from ..types import messages as messages_ +from ..types.tools import ToolLike as ToolLike +from ..types.tools import ToolSchema as ToolSchema +from . import runtime + + +class Tool[**P, R]: + def __init__( + self, + fn: Callable[P, Awaitable[R]], + schema: ToolSchema, + validator: type[pydantic.BaseModel] | None = None, + ) -> None: + self._fn = fn + self._is_gen = inspect.isasyncgenfunction(fn) + self._validator = validator + self.schema = schema + + async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: + return await self._fn(*args, **kwargs) + + async def validate_and_call(self, json_str: str) -> Any: + kwargs = json.loads(json_str) if json_str else {} + if self._validator is not None: + self._validator.model_validate(kwargs) + + if self._is_gen: + return await self._drain_generator(kwargs) + return await self._fn(**kwargs) # type: ignore[call-arg] + + async def _drain_generator(self, kwargs: dict[str, Any]) -> Any: + sink = runtime.get_sink() + final: Any = None + gen = self._fn(**kwargs) # type: ignore[call-arg] + async for msg in gen: # type: ignore[attr-defined] + final = msg + if sink is not None: + await sink.put(msg) + return final + + @property + def name(self) -> str: + return self.schema.name + + @property + def description(self) -> str: + return self.schema.description + + @property + def param_schema(self) -> dict[str, Any]: + return self.schema.param_schema + + +@overload +def tool[**P, R](fn: Callable[P, Awaitable[R]]) -> Tool[P, R]: ... +@overload +def tool[**P, R](fn: Callable[P, AsyncGenerator[R]]) -> Tool[P, R]: ... + + +def tool[**P, R](fn: Callable[P, Any]) -> Tool[P, R]: + """Decorator to define a tool from an async function or async generator.""" + sig = inspect.signature(fn) + hints = get_type_hints(fn) if hasattr(fn, "__annotations__") else {} + + fields: dict[str, Any] = {} + for param_name, param in sig.parameters.items(): + param_type = hints.get(param_name, str) + if param.default is inspect.Parameter.empty: + fields[param_name] = (param_type, ...) + else: + fields[param_name] = (param_type, param.default) + + validator = pydantic.create_model(f"{fn.__name__}_Args", **fields) + + schema = ToolSchema( + name=fn.__name__, + description=inspect.getdoc(fn) or "", + param_schema=validator.model_json_schema(), + return_type=hints.get("return", None), + ) + + return Tool(fn=fn, schema=schema, validator=validator) + + +class ToolCall: + """Callable bridge between a ToolPart (data from model) and a Tool (executable).""" + + def __init__(self, part: messages_.ToolPart, tool: Tool[..., Any]) -> None: + self._part = part + self._tool = tool + + @property + def id(self) -> str: + return self._part.tool_call_id + + @property + def name(self) -> str: + return self._part.tool_name + + @property + def args(self) -> str: + return self._part.tool_args + + async def __call__(self) -> messages_.Message: + result = await self._tool.validate_and_call(self._part.tool_args) + updated_part = self._part.with_result(result) + return messages_.Message(role="assistant", parts=[updated_part]) diff --git a/src/vercel_ai_sdk/models/__init__.py b/src/vercel_ai_sdk/models/__init__.py index 9e921afd..6e4b4665 100644 --- a/src/vercel_ai_sdk/models/__init__.py +++ b/src/vercel_ai_sdk/models/__init__.py @@ -115,7 +115,44 @@ def _auto_client(model: Model) -> Client: # --------------------------------------------------------------------------- -async def stream( +class StreamResult: + """Wrapper around a message stream. Async-iterable; collects the final result. + + Properties like ``.text`` and ``.tool_calls`` delegate to the final + ``Message`` snapshot and are available after iteration completes. + """ + + def __init__(self, gen: AsyncGenerator[messages_.Message]) -> None: + self._gen = gen + self._final: messages_.Message | None = None + + def __aiter__(self) -> AsyncGenerator[messages_.Message]: + return self._iterate() + + async def _iterate(self) -> AsyncGenerator[messages_.Message]: + async for msg in self._gen: + self._final = msg + yield msg + + @property + def text(self) -> str: + return self._final.text if self._final else "" + + @property + def tool_calls(self) -> list[messages_.ToolPart]: + return self._final.tool_calls if self._final else [] + + @property + def usage(self) -> messages_.Usage | None: + return self._final.usage if self._final else None + + @property + def output(self) -> Any: + """Parsed structured output from the final message, if available.""" + return self._final.output if self._final else None + + +def stream( model: Model, messages: list[messages_.Message], *, @@ -123,12 +160,12 @@ async def stream( output_type: type[pydantic.BaseModel] | None = None, client: Client | None = None, **kwargs: Any, -) -> AsyncGenerator[messages_.Message]: +) -> StreamResult: """Stream an LLM response. - Resolves the adapter function from ``model.adapter``, auto-creates a - :class:`Client` from env vars if none is provided, and yields - ``Message`` snapshots. + Returns a :class:`StreamResult` that is async-iterable and collects + the final ``Message``. After iteration, access ``.text``, + ``.tool_calls``, ``.usage``, etc. """ _ensure_adapters() c = client or _auto_client(model) @@ -139,10 +176,9 @@ async def stream( f"No stream adapter registered for adapter={model.adapter!r}. " f"Registered: {registered}" ) - async for msg in adapter_fn( - c, model, messages, tools=tools, output_type=output_type, **kwargs - ): - yield msg + return StreamResult( + adapter_fn(c, model, messages, tools=tools, output_type=output_type, **kwargs) + ) async def generate( @@ -197,6 +233,7 @@ async def buffer(gen: AsyncGenerator[messages_.Message]) -> messages_.Message: "Model", "ModelCost", "StreamFn", + "StreamResult", "VideoParams", # Public API "buffer", From 286390e9b0ec1b32dca1367dcb01ab376827f7bf Mon Sep 17 00:00:00 2001 From: Andrey Buzin Date: Tue, 7 Apr 2026 15:44:25 -0700 Subject: [PATCH 2/6] Port tools support --- src/vercel_ai_sdk/agents3/__init__.py | 5 +- src/vercel_ai_sdk/agents3/agent.py | 147 +++++++++++++++++++++++--- src/vercel_ai_sdk/agents3/runtime.py | 1 - src/vercel_ai_sdk/agents3/tools.py | 118 --------------------- 4 files changed, 132 insertions(+), 139 deletions(-) delete mode 100644 src/vercel_ai_sdk/agents3/tools.py diff --git a/src/vercel_ai_sdk/agents3/__init__.py b/src/vercel_ai_sdk/agents3/__init__.py index 7fdbbb96..f933bca1 100644 --- a/src/vercel_ai_sdk/agents3/__init__.py +++ b/src/vercel_ai_sdk/agents3/__init__.py @@ -1,13 +1,10 @@ -from .agent import Agent, Context, StreamResult, agent, stream -from .tools import Tool, ToolCall, tool +from .agent import Agent, Context, Tool, ToolCall, agent, tool __all__ = [ "Agent", "Context", - "StreamResult", "Tool", "ToolCall", "agent", - "stream", "tool", ] diff --git a/src/vercel_ai_sdk/agents3/agent.py b/src/vercel_ai_sdk/agents3/agent.py index 806b595c..73241d6d 100644 --- a/src/vercel_ai_sdk/agents3/agent.py +++ b/src/vercel_ai_sdk/agents3/agent.py @@ -3,25 +3,139 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncGenerator, AsyncIterable, Sequence -from typing import Any, Protocol +import inspect +import json +from collections.abc import AsyncGenerator, Awaitable, Callable +from typing import Any, Protocol, get_type_hints import pydantic from .. import models, types -from . import runtime, tools as tools_ +from . import runtime + + +class Tool[**P, R]: + """Wraps async function, introspects schema, attaches a validator""" + + def __init__( + self, + fn: Callable[P, Awaitable[R]], + schema: types.ToolSchema, + validator: type[pydantic.BaseModel] | None = None, + *, + is_gen: bool = False, + ) -> None: + self._fn = fn + self._validator = validator + self._is_gen = is_gen + self.schema = schema + + async def __call__(self, json_args: str) -> R: + """Parse json_args into kwargs, validate, and call the function.""" + kwargs = json.loads(json_args) if json_args else {} + if self._validator is not None: + self._validator.model_validate(kwargs) + + if not self._is_gen: + return await self._fn(**kwargs) + + # Generator tool (e.g. agent-as-a-tool): drain the async + # generator, pipe each yielded message to the runtime for + # real-time streaming, and collect all messages as the result. + rt = runtime.get_runtime() + messages: list[types.Message] = [] + async for message in self._fn(**kwargs): + await rt.put_message(message) + messages.append(message) + return messages + + @property + def name(self) -> str: + return self.schema.name + + @property + def description(self) -> str: + return self.schema.description + + @property + def param_schema(self) -> dict[str, Any]: + return self.schema.param_schema + + +def tool[**P, R](fn: Callable[P, Awaitable[R]]) -> Tool[P, R]: + """Decorator: turn an async function into a :class:`Tool`.""" + sig = inspect.signature(fn) + hints = get_type_hints(fn) if hasattr(fn, "__annotations__") else {} + + fields: dict[str, Any] = {} + for param_name, param in sig.parameters.items(): + param_type = hints.get(param_name, str) + if param.default is inspect.Parameter.empty: + fields[param_name] = (param_type, ...) + else: + fields[param_name] = (param_type, param.default) + + validator = pydantic.create_model(f"{fn.__name__}_Args", **fields) + + schema = types.ToolSchema( + name=fn.__name__, + description=inspect.getdoc(fn) or "", + param_schema=validator.model_json_schema(), + return_type=hints.get("return", None), + ) + + return Tool( + fn=fn, + schema=schema, + validator=validator, + is_gen=inspect.isasyncgenfunction(fn), + ) + + +class ToolCall: + """ + Intermediate object that context resolves tool parts to. + Binds together ToolPart and Tool, allows users to inspect it. + """ + + def __init__(self, part: types.ToolPart, tool: Tool[..., Any]) -> None: + self._part = part + self._tool = tool + + @property + def id(self) -> str: + return self._part.tool_call_id + + @property + def name(self) -> str: + return self._part.tool_name + + @property + def args(self) -> str: + return self._part.tool_args + + async def __call__(self) -> types.Message: + """Execute the tool and return a message with the resolved part.""" + result = await self._tool(self._part.tool_args) + updated = self._part.with_result(result) + return types.Message(role="assistant", parts=[updated]) class Context(pydantic.BaseModel): - """Everything that goes into the LLM""" + """Everything that goes into the LLM.""" messages: list[types.Message] - tools: list[ - tools_.Tool[..., Any] - ] # TODO should be something serializable like schema + tools: list[Tool[..., Any]] model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + def resolve(self, tool_parts: list[types.ToolPart]) -> list[ToolCall]: + """Resolve ToolParts into callable ToolCall objects.""" + tools_by_name = {t.name: t for t in self.tools} + return [ + ToolCall(part=tp, tool=tools_by_name[tp.tool_name]) for tp in tool_parts + ] + class LoopFn(Protocol): def __call__(self, context: Context) -> AsyncGenerator[types.Message]: ... @@ -33,10 +147,15 @@ async def _default_loop(context: Context) -> AsyncGenerator[types.Message]: async for message in stream: yield message + tool_calls = context.resolve(stream.message.tool_calls) + if not tool_calls: + break + async with asyncio.TaskGroup() as tg: - pass - # todo call tools - # yield tool messages + tasks = [tg.create_task(tc()) for tc in tool_calls] + + for task in tasks: + yield task.result class Agent: @@ -45,9 +164,9 @@ class Agent: def __init__( self, *, - tools: list[tools_.Tool[..., Any]] | None = None, + tools: list[Tool[..., Any]] | None = None, ) -> None: - self._tools: list[tools_.Tool[..., Any]] = tools or [] + self._tools: list[Tool[..., Any]] = tools or [] self._loop_fn: LoopFn = _default_loop def loop(self, fn: LoopFn) -> LoopFn: @@ -59,10 +178,6 @@ async def run( self, model: models.Model, messages: list[types.Message] ) -> AsyncGenerator[types.Message]: """Run the agent loop, yielding messages to the consumer.""" - - # todo: validate messages, maybe inject standard system message - # todo: check tools, allow passing filtered list of tools - context = Context(messages=messages, tools=self._tools) async for message in runtime.run(self._loop_fn(context)): diff --git a/src/vercel_ai_sdk/agents3/runtime.py b/src/vercel_ai_sdk/agents3/runtime.py index 964430ca..71c26ec6 100644 --- a/src/vercel_ai_sdk/agents3/runtime.py +++ b/src/vercel_ai_sdk/agents3/runtime.py @@ -5,7 +5,6 @@ import asyncio import contextvars from collections.abc import AsyncGenerator, AsyncIterable, Awaitable -from typing import Any from .. import types diff --git a/src/vercel_ai_sdk/agents3/tools.py b/src/vercel_ai_sdk/agents3/tools.py deleted file mode 100644 index 63d30328..00000000 --- a/src/vercel_ai_sdk/agents3/tools.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Tool decorator, Tool class, and ToolCall callable wrapper.""" - -from __future__ import annotations - -import inspect -import json -from collections.abc import AsyncGenerator, Awaitable, Callable -from typing import Any, get_type_hints, overload - -import pydantic - -from ..types import messages as messages_ -from ..types.tools import ToolLike as ToolLike -from ..types.tools import ToolSchema as ToolSchema -from . import runtime - - -class Tool[**P, R]: - def __init__( - self, - fn: Callable[P, Awaitable[R]], - schema: ToolSchema, - validator: type[pydantic.BaseModel] | None = None, - ) -> None: - self._fn = fn - self._is_gen = inspect.isasyncgenfunction(fn) - self._validator = validator - self.schema = schema - - async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: - return await self._fn(*args, **kwargs) - - async def validate_and_call(self, json_str: str) -> Any: - kwargs = json.loads(json_str) if json_str else {} - if self._validator is not None: - self._validator.model_validate(kwargs) - - if self._is_gen: - return await self._drain_generator(kwargs) - return await self._fn(**kwargs) # type: ignore[call-arg] - - async def _drain_generator(self, kwargs: dict[str, Any]) -> Any: - sink = runtime.get_sink() - final: Any = None - gen = self._fn(**kwargs) # type: ignore[call-arg] - async for msg in gen: # type: ignore[attr-defined] - final = msg - if sink is not None: - await sink.put(msg) - return final - - @property - def name(self) -> str: - return self.schema.name - - @property - def description(self) -> str: - return self.schema.description - - @property - def param_schema(self) -> dict[str, Any]: - return self.schema.param_schema - - -@overload -def tool[**P, R](fn: Callable[P, Awaitable[R]]) -> Tool[P, R]: ... -@overload -def tool[**P, R](fn: Callable[P, AsyncGenerator[R]]) -> Tool[P, R]: ... - - -def tool[**P, R](fn: Callable[P, Any]) -> Tool[P, R]: - """Decorator to define a tool from an async function or async generator.""" - sig = inspect.signature(fn) - hints = get_type_hints(fn) if hasattr(fn, "__annotations__") else {} - - fields: dict[str, Any] = {} - for param_name, param in sig.parameters.items(): - param_type = hints.get(param_name, str) - if param.default is inspect.Parameter.empty: - fields[param_name] = (param_type, ...) - else: - fields[param_name] = (param_type, param.default) - - validator = pydantic.create_model(f"{fn.__name__}_Args", **fields) - - schema = ToolSchema( - name=fn.__name__, - description=inspect.getdoc(fn) or "", - param_schema=validator.model_json_schema(), - return_type=hints.get("return", None), - ) - - return Tool(fn=fn, schema=schema, validator=validator) - - -class ToolCall: - """Callable bridge between a ToolPart (data from model) and a Tool (executable).""" - - def __init__(self, part: messages_.ToolPart, tool: Tool[..., Any]) -> None: - self._part = part - self._tool = tool - - @property - def id(self) -> str: - return self._part.tool_call_id - - @property - def name(self) -> str: - return self._part.tool_name - - @property - def args(self) -> str: - return self._part.tool_args - - async def __call__(self) -> messages_.Message: - result = await self._tool.validate_and_call(self._part.tool_args) - updated_part = self._part.with_result(result) - return messages_.Message(role="assistant", parts=[updated_part]) From e53079c477f97708006c9b27667c376f28130dbd Mon Sep 17 00:00:00 2001 From: Andrey Buzin Date: Tue, 7 Apr 2026 16:15:12 -0700 Subject: [PATCH 3/6] Implement auto-append for yielded messages Split tool calls and tool results into separate messages --- examples/agents3/custom_loop.py | 65 +++++ examples/agents3/nested_agents.py | 53 +++++ examples/agents3/simple.py | 36 +++ src/vercel_ai_sdk/__init__.py | 23 +- .../adapters/ai_sdk_ui/adapter.py | 224 +++++++++++------- src/vercel_ai_sdk/agents/agent.py | 12 +- src/vercel_ai_sdk/agents/runtime.py | 45 ++-- src/vercel_ai_sdk/agents/streams.py | 2 +- src/vercel_ai_sdk/agents3/agent.py | 80 +++++-- src/vercel_ai_sdk/models/__init__.py | 2 +- src/vercel_ai_sdk/models/ai_gateway/stream.py | 57 +++-- src/vercel_ai_sdk/models/anthropic/adapter.py | 31 +-- .../models/core/helpers/streaming.py | 4 +- src/vercel_ai_sdk/models/openai/adapter.py | 27 ++- src/vercel_ai_sdk/types/__init__.py | 6 +- src/vercel_ai_sdk/types/builders.py | 35 ++- src/vercel_ai_sdk/types/messages.py | 67 ++++-- tests/adapters/ai_sdk_ui/test_adapter.py | 76 +++--- tests/agents/mcp/test_client.py | 12 +- tests/agents/test_checkpoint.py | 4 +- tests/agents/test_runtime.py | 52 ++-- tests/agents/test_streams.py | 4 +- tests/conftest.py | 30 ++- tests/models/ai_gateway/test_protocol.py | 35 ++- tests/models/ai_gateway/test_stream.py | 10 +- tests/models/core/test_streaming.py | 16 +- tests/telemetry/test_otel_handler.py | 4 +- tests/telemetry/test_telemetry.py | 4 +- tests/types/test_builders.py | 8 +- tests/types/test_messages.py | 81 +++---- 30 files changed, 724 insertions(+), 381 deletions(-) create mode 100644 examples/agents3/custom_loop.py create mode 100644 examples/agents3/nested_agents.py create mode 100644 examples/agents3/simple.py diff --git a/examples/agents3/custom_loop.py b/examples/agents3/custom_loop.py new file mode 100644 index 00000000..24020484 --- /dev/null +++ b/examples/agents3/custom_loop.py @@ -0,0 +1,65 @@ +"""Custom loop: manual control over streaming and tool execution.""" + +import asyncio +from collections.abc import AsyncGenerator + +import vercel_ai_sdk as ai +from vercel_ai_sdk.agents3 import Context, agent, tool + + +@tool +async def get_weather(city: str) -> str: + """Get current weather for a city.""" + return f"Sunny, 72F in {city}" + + +@tool +async def get_population(city: str) -> int: + """Get population of a city.""" + return {"new york": 8_336_817, "tokyo": 13_960_000}.get(city.lower(), 1_000_000) + + +async def main() -> None: + model = ai.Model( + id="anthropic/claude-sonnet-4-20250514", + adapter="ai-gateway-v3", + provider="ai-gateway", + ) + + tools = [get_weather, get_population] + my_agent = agent(tools=tools) + + @my_agent.loop + async def custom(context: Context) -> AsyncGenerator[ai.Message]: + """Stream, execute tools with logging, repeat.""" + while True: + s = ai.models.stream(context.model, context.messages, tools=context.tools) + async for msg in s: + yield msg + + tool_calls = context.resolve(s.tool_calls) + if not tool_calls: + return + + print( + f"\n [calling {len(tool_calls)} tool(s): " + f"{', '.join(tc.name for tc in tool_calls)}]" + ) + + async with asyncio.TaskGroup() as tg: + tasks = [tg.create_task(tc()) for tc in tool_calls] + + # Yield a tool-result message — history auto-collects it. + yield ai.tool_message(*(t.result() for t in tasks)) + + async for msg in my_agent.run( + model, + [ai.user_message("Compare the weather and population of New York and Tokyo.")], + ): + if msg.text_delta: + print(msg.text_delta, end="", flush=True) + print() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/agents3/nested_agents.py b/examples/agents3/nested_agents.py new file mode 100644 index 00000000..32a3d2a5 --- /dev/null +++ b/examples/agents3/nested_agents.py @@ -0,0 +1,53 @@ +"""Nested agents: a research tool that is itself an agent streaming through the sink.""" + +import asyncio +from collections.abc import AsyncGenerator + +import vercel_ai_sdk as ai +from vercel_ai_sdk.agents3 import agent, tool + +model = ai.Model( + id="anthropic/claude-sonnet-4-20250514", + adapter="ai-gateway-v3", + provider="ai-gateway", +) + + +@tool +async def get_facts(topic: str) -> str: + """Look up facts about a topic.""" + facts = { + "mars": "Mars has two moons: Phobos and Deimos. A day on Mars is 24.6 hours.", + "venus": "Venus rotates backwards. Its surface temperature is 450C.", + } + return facts.get(topic.lower(), f"No facts found for {topic}.") + + +# This tool is an async generator — it streams intermediate messages +# through the runtime sink, then returns the final result. +@tool +async def research(topic: str) -> AsyncGenerator[ai.Message]: + """Research a topic in depth using a sub-agent.""" + researcher = agent( + system="You are a research assistant. Be concise.", + tools=[get_facts], + ) + + async for msg in researcher.run(model, [ai.user_message(f"Research: {topic}")]): + yield msg + + +async def main() -> None: + orchestrator = agent( + system="Use the research tool to answer questions. Summarize the findings.", + tools=[research], + ) + + async for msg in orchestrator.run(model, [ai.user_message("Tell me about Mars.")]): + if msg.text_delta: + print(msg.text_delta, end="", flush=True) + print() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/agents3/simple.py b/examples/agents3/simple.py new file mode 100644 index 00000000..dc2fc5ad --- /dev/null +++ b/examples/agents3/simple.py @@ -0,0 +1,36 @@ +"""Simplest agent: model + tool, default loop handles everything.""" + +import asyncio + +import vercel_ai_sdk as ai +from vercel_ai_sdk.agents3 import agent, tool + + +@tool +async def get_weather(city: str) -> str: + """Get current weather for a city.""" + return f"Sunny, 72F in {city}" + + +async def main() -> None: + model = ai.Model( + id="anthropic/claude-sonnet-4-20250514", + adapter="ai-gateway-v3", + provider="ai-gateway", + ) + + my_agent = agent( + system="You are a helpful weather assistant.", + tools=[get_weather], + ) + + async for msg in my_agent.run( + model, [ai.user_message("What's the weather in Tokyo?")] + ): + if msg.text_delta: + print(msg.text_delta, end="", flush=True) + print() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/vercel_ai_sdk/__init__.py b/src/vercel_ai_sdk/__init__.py index 5dcc0c59..fe2163c9 100644 --- a/src/vercel_ai_sdk/__init__.py +++ b/src/vercel_ai_sdk/__init__.py @@ -37,13 +37,23 @@ ReasoningPart, StructuredOutputPart, TextPart, + ToolCallPart, ToolDelta, ToolLike, - ToolPart, + ToolResultPart, ToolSchema, Usage, make_messages, ) +from .types.builders import ( + assistant_message, + file_part, + system_message, + thinking, + tool_message, + tool_result, + user_message, +) __all__ = [ # Types (from types/) @@ -51,7 +61,8 @@ "Part", "PartState", "TextPart", - "ToolPart", + "ToolCallPart", + "ToolResultPart", "ToolDelta", "ReasoningPart", "FilePart", @@ -61,6 +72,14 @@ "ToolSchema", "Usage", "make_messages", + # Builders (from types/builders) + "user_message", + "assistant_message", + "system_message", + "tool_message", + "tool_result", + "file_part", + "thinking", # Models (from models/) "Model", "ModelCost", diff --git a/src/vercel_ai_sdk/adapters/ai_sdk_ui/adapter.py b/src/vercel_ai_sdk/adapters/ai_sdk_ui/adapter.py index ba0ec8e4..59c78e57 100644 --- a/src/vercel_ai_sdk/adapters/ai_sdk_ui/adapter.py +++ b/src/vercel_ai_sdk/adapters/ai_sdk_ui/adapter.py @@ -8,7 +8,7 @@ import json import logging from collections.abc import AsyncGenerator, AsyncIterable -from typing import Any, Literal +from typing import Any from ...agents import hooks from ...types import messages as messages_ @@ -169,6 +169,13 @@ async def to_ui_message_stream( state = _StreamState() async for msg in messages: + # Tool-result messages (role="tool") are emitted by the Runtime + # as separate Message objects (with their own auto-generated id). + # To the frontend they belong to the *same* step as the tool + # call, so we pin the message id to avoid a spurious step boundary. + if msg.role == "tool" and state.message_id: + msg = msg.model_copy(update={"id": state.message_id}) + # Tool-approval hook messages are emitted by the Runtime as # separate Message objects (with their own id). To the frontend # they belong to the *same* step as the tool call, so we pin @@ -217,26 +224,21 @@ async def to_ui_message_stream( for part in state.close_open_blocks(): yield part - # Scan tool parts for new pending/completed states - has_new_pending_tools = False - has_new_tool_results = False - - for msg_part in msg.parts: - if isinstance(msg_part, messages_.ToolPart): - if ( - msg_part.status == "pending" - and msg_part.tool_call_id not in state.pending_tool_calls - ): - has_new_pending_tools = True - elif ( - msg_part.status in ("result", "error") - and msg_part.tool_call_id not in state.emitted_tool_results - ): - has_new_tool_results = True + # Scan for new pending tool calls or tool results + has_new_pending_tools = any( + isinstance(p, messages_.ToolCallPart) + and p.tool_call_id not in state.pending_tool_calls + for p in msg.parts + ) + has_new_tool_results = any( + isinstance(p, messages_.ToolResultPart) + and p.tool_call_id not in state.emitted_tool_results + for p in msg.parts + ) - # Process parts in two passes: - # 1. First handle text and pending tools - # 2. Then handle tool results (which may need their own step) + # Process parts in passes: + # 1. Text and pending tool calls (from assistant messages) + # 2. Tool results (from tool messages) # Pass 1: Text and pending tool inputs for msg_part in msg.parts: @@ -250,8 +252,7 @@ async def to_ui_message_stream( text_id = messages_.generate_id("text") yield protocol.TextStartPart(id=text_id) yield protocol.TextEndPart(id=text_id) - case messages_.ToolPart( - status="pending", + case messages_.ToolCallPart( tool_call_id=tc_id, tool_name=name, tool_args=args, @@ -270,25 +271,19 @@ async def to_ui_message_stream( input=args, ) - # Pass 2: Tool outputs (same step as tool input per AI SDK protocol) - # Tool input and output are part of the same "step" (one LLM turn) + # Pass 2: Tool results if has_new_tool_results: for msg_part in msg.parts: - match msg_part: - case messages_.ToolPart( - tool_call_id=tc_id, - result=result, - status=status, - ) if ( - status in ("result", "error") - and tc_id not in state.emitted_tool_results - ): - state.emitted_tool_results.add(tc_id) - state.pending_tool_calls.discard(tc_id) - yield protocol.ToolOutputAvailablePart( - tool_call_id=tc_id, - output=result, - ) + if ( + isinstance(msg_part, messages_.ToolResultPart) + and msg_part.tool_call_id not in state.emitted_tool_results + ): + state.emitted_tool_results.add(msg_part.tool_call_id) + state.pending_tool_calls.discard(msg_part.tool_call_id) + yield protocol.ToolOutputAvailablePart( + tool_call_id=msg_part.tool_call_id, + output=msg_part.result, + ) # Pass 3: Hook-based tool approvals for msg_part in msg.parts: @@ -356,15 +351,14 @@ async def to_sse_stream( _TOOL_ERROR_STATES: frozenset[str] = frozenset({"output-error", "output-denied"}) -def _map_tool_status( - state: ui_message.UIToolInvocationState, -) -> Literal["pending", "result", "error"]: - """Map AI SDK v6 tool invocation state to internal status.""" - if state in _TOOL_ERROR_STATES: - return "error" - if state in _TOOL_RESULT_STATES: - return "result" - return "pending" +def _is_tool_completed(state: ui_message.UIToolInvocationState) -> bool: + """Return True if the tool invocation state indicates a completed tool.""" + return state in _TOOL_RESULT_STATES or state in _TOOL_ERROR_STATES + + +def _is_tool_error(state: ui_message.UIToolInvocationState) -> bool: + """Return True if the tool invocation state indicates an error.""" + return state in _TOOL_ERROR_STATES def _normalize_tool_args(tool_input: str | dict[str, Any] | None) -> str: @@ -379,9 +373,9 @@ def _normalize_tool_args(tool_input: str | dict[str, Any] | None) -> str: def _normalize_tool_result(output: Any) -> dict[str, Any] | None: - """Normalize tool output to dict format for internal ToolPart. + """Normalize tool output to dict format for internal ToolResultPart. - The internal ToolPart.result expects dict | None, but AI SDK + The internal ToolResultPart.result expects dict | None, but AI SDK output can be any type. Wrap non-dict results for compatibility. """ if output is None: @@ -412,39 +406,56 @@ def to_messages( resolved_any_approval = False for ui_msg in ui_messages: - internal_parts: list[messages_.Part] = [] + # For assistant messages, separate tool calls from tool results. + assistant_parts: list[messages_.Part] = [] + tool_result_parts: list[messages_.ToolResultPart] = [] for part in ui_msg.parts: match part: case ui_message.UITextPart(text=text) if text: - internal_parts.append(messages_.TextPart(text=text)) + assistant_parts.append(messages_.TextPart(text=text)) case ui_message.UIReasoningPart(reasoning=reasoning): - internal_parts.append(messages_.ReasoningPart(text=reasoning)) + assistant_parts.append(messages_.ReasoningPart(text=reasoning)) case ui_message.UIToolInvocationPart() as inv: - # Legacy tool-invocation type - internal_parts.append( - messages_.ToolPart( + # Legacy tool-invocation type — always create the call part + tool_args = json.dumps(inv.args) if inv.args else "{}" + assistant_parts.append( + messages_.ToolCallPart( tool_call_id=inv.tool_invocation_id, tool_name=inv.tool_name, - tool_args=json.dumps(inv.args) if inv.args else "{}", - status=_map_tool_status(inv.state), - result=inv.result, + tool_args=tool_args, ) ) + if _is_tool_completed(inv.state): + tool_result_parts.append( + messages_.ToolResultPart( + tool_call_id=inv.tool_invocation_id, + tool_name=inv.tool_name, + result=inv.result, + is_error=_is_tool_error(inv.state), + ) + ) case ui_message.UIToolPart() as tp: # Dynamic tool-{toolName} type (e.g., "tool-get_weather") - internal_parts.append( - messages_.ToolPart( + assistant_parts.append( + messages_.ToolCallPart( tool_call_id=tp.tool_call_id, tool_name=tp.tool_name, tool_args=_normalize_tool_args(tp.input), - status=_map_tool_status(tp.state), - result=_normalize_tool_result(tp.output), ) ) + if _is_tool_completed(tp.state): + tool_result_parts.append( + messages_.ToolResultPart( + tool_call_id=tp.tool_call_id, + tool_name=tp.tool_name, + result=_normalize_tool_result(tp.output), + is_error=_is_tool_error(tp.state), + ) + ) # Side-effect: resolve ToolApproval hooks from approval # responses so the agent loop can resume execution. if ( @@ -462,7 +473,7 @@ def to_messages( resolved_any_approval = True case ui_message.UIFilePart() as fp: - internal_parts.append( + assistant_parts.append( messages_.FilePart( data=fp.url, media_type=fp.media_type, @@ -477,9 +488,8 @@ def to_messages( ): pass # Skip unsupported/boundary parts - # Validate user/system messages have content - OpenAI requires it there. - # Assistant messages can have empty content if they have tool calls. - if ui_msg.role in ("user", "system") and not internal_parts: + # Validate user/system messages have content - OpenAI requires it. + if ui_msg.role in ("user", "system") and not assistant_parts: raise ValueError( f"Message '{ui_msg.id}' has role '{ui_msg.role}' but no content. " "User and system messages require non-empty content." @@ -487,16 +497,21 @@ def to_messages( # The UI sends one assistant message per conversation turn, but a # single turn may span multiple default-loop iterations (e.g. - # [text, tool(done), text, tool(done), text]). LLM APIs expect - # one message per iteration, so split at completed-tool boundaries. + # [text, tool_call, tool_result, text, tool_call, tool_result, text]). + # LLM APIs expect one message per iteration, so split into + # assistant + tool message pairs at tool-result boundaries. if ui_msg.role == "assistant": - result.extend(_split_assistant_parts(internal_parts, msg_id=ui_msg.id)) + result.extend( + _split_assistant_parts( + assistant_parts, tool_result_parts, msg_id=ui_msg.id + ) + ) else: result.append( messages_.Message( id=ui_msg.id, role=ui_msg.role, - parts=internal_parts, + parts=assistant_parts, ) ) @@ -515,34 +530,79 @@ def to_messages( def _split_assistant_parts( parts: list[messages_.Part], + tool_results: list[messages_.ToolResultPart], msg_id: str, ) -> list[messages_.Message]: - """Split assistant parts at completed-tool → non-tool boundaries. + """Split assistant parts into assistant + tool message pairs. - Returns one ``Message`` per default-loop iteration so that LLM - adapters receive correctly-shaped single-iteration messages. + The UI sends one big assistant message per turn, but internally each + loop iteration produces an assistant message (with tool calls) followed + by a tool message (with results). This reconstructs that structure. + + Returns a list of Messages: alternating assistant and tool messages, + split at tool-call boundaries when results are available. """ + # Index tool results by their tool_call_id for lookup + results_by_id = {tr.tool_call_id: tr for tr in tool_results} + messages: list[messages_.Message] = [] current: list[messages_.Part] = [] - has_completed_tool = False + pending_results: list[messages_.ToolResultPart] = [] + + for part in parts: + current.append(part) + + # When we see a ToolCallPart that has a result, accumulate it + if ( + isinstance(part, messages_.ToolCallPart) + and part.tool_call_id in results_by_id + ): + pending_results.append(results_by_id[part.tool_call_id]) + + # If there are pending results and more parts follow, we need to split. + # Walk again, splitting at boundaries where all accumulated tool calls + # have results and a non-tool part follows. + if not pending_results: + # No completed tools — single assistant message + if current: + messages.append( + messages_.Message(role="assistant", parts=current, id=msg_id) + ) + return messages + + # Re-walk to split at tool-call boundaries + messages = [] + current = [] + current_results: list[messages_.ToolResultPart] = [] + seen_tool_call = False for part in parts: - if has_completed_tool and not isinstance(part, messages_.ToolPart): + # If we had a completed tool call group and now see a non-tool part, + # split here + if ( + seen_tool_call + and current_results + and not isinstance(part, messages_.ToolCallPart) + ): messages.append( messages_.Message(role="assistant", parts=current, id=msg_id) ) + messages.append(messages_.Message(role="tool", parts=list(current_results))) current = [] - has_completed_tool = False + current_results = [] + seen_tool_call = False current.append(part) - if isinstance(part, messages_.ToolPart) and part.status in ( - "result", - "error", - ): - has_completed_tool = True + if isinstance(part, messages_.ToolCallPart): + seen_tool_call = True + if part.tool_call_id in results_by_id: + current_results.append(results_by_id[part.tool_call_id]) + # Flush remaining if current: messages.append(messages_.Message(role="assistant", parts=current, id=msg_id)) + if current_results: + messages.append(messages_.Message(role="tool", parts=list(current_results))) return messages diff --git a/src/vercel_ai_sdk/agents/agent.py b/src/vercel_ai_sdk/agents/agent.py index 5d9e2688..eacca666 100644 --- a/src/vercel_ai_sdk/agents/agent.py +++ b/src/vercel_ai_sdk/agents/agent.py @@ -194,16 +194,18 @@ async def _default_loop( if last_msg is None: return result - updated_parts = await asyncio.gather( + result_parts = await asyncio.gather( *( runtime.execute_tool(tc, message=last_msg) for tc in result.tool_calls ) ) - updated_msg = last_msg - for updated_tc in updated_parts: - updated_msg = updated_msg.replace(updated_tc) - local_messages.append(updated_msg) + # Append the assistant message (with tool calls) and a + # separate tool-result message to the local history. + local_messages.append(last_msg) + local_messages.append( + messages_.Message(role="tool", parts=list(result_parts)) + ) def run( self, diff --git a/src/vercel_ai_sdk/agents/runtime.py b/src/vercel_ai_sdk/agents/runtime.py index 19098aa7..8362adc0 100644 --- a/src/vercel_ai_sdk/agents/runtime.py +++ b/src/vercel_ai_sdk/agents/runtime.py @@ -281,29 +281,30 @@ def _find_runtime_param(fn: Callable[..., Any]) -> str | None: async def execute_tool( - tool_call: messages_.ToolPart, + tool_call: messages_.ToolCallPart, message: messages_.Message | None = None, -) -> messages_.ToolPart: +) -> messages_.ToolResultPart: """Execute a single tool call with replay support. Looks up the tool by name — first from the active Context (if any), - then from the global registry. Returns an updated (immutable) - ToolPart with the result filled in. Emits the updated message to - the LoopExecutor queue so the UI sees the transition from - status="pending" to status="result" (or "error"). + then from the global registry. Returns a :class:`ToolResultPart` + with the result. If a checkpoint exists with a cached result for this tool_call_id, returns the cached result without re-executing. """ rt = _runtime.get(None) - # Replay: return updated part from cache + # Replay: return result part from cache if rt: cached = rt.log.try_replay_tool(tool_call.tool_call_id) if cached is not None: - if cached.status == "error": - return tool_call.with_error(cached.result) - return tool_call.with_result(cached.result) + return messages_.ToolResultPart( + tool_call_id=tool_call.tool_call_id, + tool_name=tool_call.tool_name, + result=cached.result, + is_error=cached.status == "error", + ) telemetry.handle( telemetry.ToolCallStartEvent( @@ -325,13 +326,20 @@ async def execute_tool( raise ValueError(f"Tool not found in registry: {tool_call.tool_name}") error_str: str | None = None + is_error = False try: result = await tool.validate_and_call(tool_call.tool_args, rt) - updated = tool_call.with_result(result) except (json.JSONDecodeError, pydantic.ValidationError) as exc: result = f"{type(exc).__name__}: {exc}" error_str = result - updated = tool_call.with_error(result) + is_error = True + + result_part = messages_.ToolResultPart( + tool_call_id=tool_call.tool_call_id, + tool_name=tool_call.tool_name, + result=result, + is_error=is_error, + ) telemetry.handle( telemetry.ToolCallFinishEvent( @@ -344,14 +352,19 @@ async def execute_tool( ) # Record for checkpoint + status = "error" if is_error else "result" if rt: - rt.log.record_tool(tool_call.tool_call_id, result, status=updated.status) + rt.log.record_tool(tool_call.tool_call_id, result, status=status) - # Emit updated message so UI sees status change + # Emit tool result message so UI sees the result if rt and message: - await rt.executor.put_message(message.replace(updated)) + tool_msg = messages_.Message( + role="tool", + parts=[result_part], + ) + await rt.executor.put_message(tool_msg) - return updated + return result_part # ── RunResult ───────────────────────────────────────────────────── diff --git a/src/vercel_ai_sdk/agents/streams.py b/src/vercel_ai_sdk/agents/streams.py index fadf6747..095ed995 100644 --- a/src/vercel_ai_sdk/agents/streams.py +++ b/src/vercel_ai_sdk/agents/streams.py @@ -18,7 +18,7 @@ def last_message(self) -> messages_.Message | None: return self.messages[-1] if self.messages else None @property - def tool_calls(self) -> list[messages_.ToolPart]: + def tool_calls(self) -> list[messages_.ToolCallPart]: """Get tool calls from the last message.""" if self.last_message: return self.last_message.tool_calls diff --git a/src/vercel_ai_sdk/agents3/agent.py b/src/vercel_ai_sdk/agents3/agent.py index 73241d6d..b20382a0 100644 --- a/src/vercel_ai_sdk/agents3/agent.py +++ b/src/vercel_ai_sdk/agents3/agent.py @@ -11,6 +11,7 @@ import pydantic from .. import models, types +from ..types import builders from . import runtime @@ -41,13 +42,13 @@ async def __call__(self, json_args: str) -> R: # Generator tool (e.g. agent-as-a-tool): drain the async # generator, pipe each yielded message to the runtime for - # real-time streaming, and collect all messages as the result. + # real-time streaming, and return the final text as the result. rt = runtime.get_runtime() - messages: list[types.Message] = [] + last: types.Message | None = None async for message in self._fn(**kwargs): await rt.put_message(message) - messages.append(message) - return messages + last = message + return last.text if last else "" @property def name(self) -> str: @@ -93,12 +94,12 @@ def tool[**P, R](fn: Callable[P, Awaitable[R]]) -> Tool[P, R]: class ToolCall: - """ - Intermediate object that context resolves tool parts to. - Binds together ToolPart and Tool, allows users to inspect it. + """Callable that binds a :class:`ToolCallPart` to its :class:`Tool`. + + Calling it executes the tool and returns a :class:`ToolResultPart`. """ - def __init__(self, part: types.ToolPart, tool: Tool[..., Any]) -> None: + def __init__(self, part: types.ToolCallPart, tool: Tool[..., Any]) -> None: self._part = part self._tool = tool @@ -114,23 +115,35 @@ def name(self) -> str: def args(self) -> str: return self._part.tool_args - async def __call__(self) -> types.Message: - """Execute the tool and return a message with the resolved part.""" - result = await self._tool(self._part.tool_args) - updated = self._part.with_result(result) - return types.Message(role="assistant", parts=[updated]) + async def __call__(self) -> types.ToolResultPart: + """Execute the tool and return a :class:`ToolResultPart`.""" + try: + result = await self._tool(self._part.tool_args) + except Exception as exc: + return types.ToolResultPart( + tool_call_id=self._part.tool_call_id, + tool_name=self._part.tool_name, + result=str(exc), + is_error=True, + ) + return types.ToolResultPart( + tool_call_id=self._part.tool_call_id, + tool_name=self._part.tool_name, + result=result, + ) class Context(pydantic.BaseModel): """Everything that goes into the LLM.""" + model: models.Model messages: list[types.Message] tools: list[Tool[..., Any]] model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) - def resolve(self, tool_parts: list[types.ToolPart]) -> list[ToolCall]: - """Resolve ToolParts into callable ToolCall objects.""" + def resolve(self, tool_parts: list[types.ToolCallPart]) -> list[ToolCall]: + """Resolve ToolCallParts into callable ToolCall objects.""" tools_by_name = {t.name: t for t in self.tools} return [ ToolCall(part=tp, tool=tools_by_name[tp.tool_name]) for tp in tool_parts @@ -143,19 +156,42 @@ def __call__(self, context: Context) -> AsyncGenerator[types.Message]: ... async def _default_loop(context: Context) -> AsyncGenerator[types.Message]: while True: - stream = models.stream(context.model, context.messages) + stream = models.stream(context.model, context.messages, tools=context.tools) async for message in stream: yield message - tool_calls = context.resolve(stream.message.tool_calls) + tool_calls = context.resolve(stream.tool_calls) if not tool_calls: break + # Execute tool calls in parallel. async with asyncio.TaskGroup() as tg: tasks = [tg.create_task(tc()) for tc in tool_calls] - for task in tasks: - yield task.result + # Yield a tool-result message — history auto-collects it. + yield builders.tool_message(*(t.result() for t in tasks)) + + +async def _collect_messages( + source: AsyncGenerator[types.Message], + messages: list[types.Message], +) -> AsyncGenerator[types.Message]: + """Intercept yielded messages and collect done ones into *messages*. + + This runs on the **producer** side (same coroutine as the loop function), + so ``messages`` is always up-to-date by the time the loop reads it for + the next model call — avoiding the race that would occur if collection + happened on the consumer side of the runtime queue. + """ + async for message in source: + if message.is_done: + for i, existing in enumerate(messages): + if existing.id == message.id: + messages[i] = message + break + else: + messages.append(message) + yield message class Agent: @@ -178,15 +214,15 @@ async def run( self, model: models.Model, messages: list[types.Message] ) -> AsyncGenerator[types.Message]: """Run the agent loop, yielding messages to the consumer.""" - context = Context(messages=messages, tools=self._tools) + context = Context(model=model, messages=list(messages), tools=self._tools) - async for message in runtime.run(self._loop_fn(context)): + source = _collect_messages(self._loop_fn(context), context.messages) + async for message in runtime.run(source): yield message def agent( *, - model: models.Model, tools: list[Tool[..., Any]] | None = None, system: str | None = None, ) -> Agent: diff --git a/src/vercel_ai_sdk/models/__init__.py b/src/vercel_ai_sdk/models/__init__.py index 6e4b4665..db36eca8 100644 --- a/src/vercel_ai_sdk/models/__init__.py +++ b/src/vercel_ai_sdk/models/__init__.py @@ -139,7 +139,7 @@ def text(self) -> str: return self._final.text if self._final else "" @property - def tool_calls(self) -> list[messages_.ToolPart]: + def tool_calls(self) -> list[messages_.ToolCallPart]: return self._final.tool_calls if self._final else [] @property diff --git a/src/vercel_ai_sdk/models/ai_gateway/stream.py b/src/vercel_ai_sdk/models/ai_gateway/stream.py index f50a8658..9ba0f73e 100644 --- a/src/vercel_ai_sdk/models/ai_gateway/stream.py +++ b/src/vercel_ai_sdk/models/ai_gateway/stream.py @@ -67,19 +67,15 @@ async def _messages_to_prompt( case "assistant": assistant_content: list[dict[str, Any]] = [] - tool_results: list[dict[str, Any]] = [] - for part in msg.parts: match part: case messages_.ReasoningPart(text=text): assistant_content.append( {"type": "reasoning", "text": text} ) - case messages_.TextPart(text=text): assistant_content.append({"type": "text", "text": text}) - - case messages_.ToolPart() as tp: + case messages_.ToolCallPart() as tp: tool_input: Any = ( json.loads(tp.tool_args) if tp.tool_args else {} ) @@ -91,32 +87,33 @@ async def _messages_to_prompt( "input": tool_input, } ) - if tp.status in ("result", "error"): - output = ( - { - "type": "error-text", - "value": ( - str(tp.result) - if tp.result is not None - else "" - ), - } - if tp.status == "error" - else { - "type": "json", - "value": tp.result, - } - ) - tool_results.append( - { - "type": "tool-result", - "toolCallId": tp.tool_call_id, - "toolName": tp.tool_name, - "output": output, - } - ) - result.append({"role": "assistant", "content": assistant_content}) + + case "tool": + tool_results: list[dict[str, Any]] = [] + for part in msg.parts: + if isinstance(part, messages_.ToolResultPart): + output = ( + { + "type": "error-text", + "value": ( + str(part.result) if part.result is not None else "" + ), + } + if part.is_error + else { + "type": "json", + "value": part.result, + } + ) + tool_results.append( + { + "type": "tool-result", + "toolCallId": part.tool_call_id, + "toolName": part.tool_name, + "output": output, + } + ) if tool_results: result.append({"role": "tool", "content": tool_results}) diff --git a/src/vercel_ai_sdk/models/anthropic/adapter.py b/src/vercel_ai_sdk/models/anthropic/adapter.py index 23c1f01b..b5d2e024 100644 --- a/src/vercel_ai_sdk/models/anthropic/adapter.py +++ b/src/vercel_ai_sdk/models/anthropic/adapter.py @@ -126,8 +126,6 @@ async def _messages_to_anthropic( ) case "assistant": content: list[dict[str, Any]] = [] - tool_results: list[dict[str, Any]] = [] - for part in msg.parts: match part: case messages_.ReasoningPart(text=text, signature=signature): @@ -141,7 +139,7 @@ async def _messages_to_anthropic( ) case messages_.TextPart(text=text): content.append({"type": "text", "text": text}) - case messages_.ToolPart(): + case messages_.ToolCallPart(): tool_input = ( json.loads(part.tool_args) if part.tool_args else {} ) @@ -153,20 +151,23 @@ async def _messages_to_anthropic( "input": tool_input, } ) - if part.status in ("result", "error"): - entry: dict[str, Any] = { - "type": "tool_result", - "tool_use_id": part.tool_call_id, - "content": str(part.result) - if part.result is not None - else "", - } - if part.status == "error": - entry["is_error"] = True - tool_results.append(entry) - if content: result.append({"role": "assistant", "content": content}) + + case "tool": + tool_results: list[dict[str, Any]] = [] + for part in msg.parts: + if isinstance(part, messages_.ToolResultPart): + entry: dict[str, Any] = { + "type": "tool_result", + "tool_use_id": part.tool_call_id, + "content": str(part.result) + if part.result is not None + else "", + } + if part.is_error: + entry["is_error"] = True + tool_results.append(entry) if tool_results: result.append({"role": "user", "content": tool_results}) diff --git a/src/vercel_ai_sdk/models/core/helpers/streaming.py b/src/vercel_ai_sdk/models/core/helpers/streaming.py index fb776295..1fa91441 100644 --- a/src/vercel_ai_sdk/models/core/helpers/streaming.py +++ b/src/vercel_ai_sdk/models/core/helpers/streaming.py @@ -212,11 +212,11 @@ def _build_message( ) ) - # Tool parts + # Tool call parts for tcid, (name, args) in self._tool_calls.items(): is_active = tcid in self._active_tool_ids parts.append( - messages_.ToolPart( + messages_.ToolCallPart( id=tcid, tool_call_id=tcid, tool_name=name, diff --git a/src/vercel_ai_sdk/models/openai/adapter.py b/src/vercel_ai_sdk/models/openai/adapter.py index c0d6261d..eb77b9f6 100644 --- a/src/vercel_ai_sdk/models/openai/adapter.py +++ b/src/vercel_ai_sdk/models/openai/adapter.py @@ -111,7 +111,6 @@ async def _messages_to_openai( content = "" reasoning = "" tool_calls: list[dict[str, Any]] = [] - tool_results: list[dict[str, Any]] = [] for part in msg.parts: match part: @@ -119,7 +118,7 @@ async def _messages_to_openai( reasoning += text case messages_.TextPart(text=text): content += text - case messages_.ToolPart(): + case messages_.ToolCallPart(): tool_calls.append( { "id": part.tool_call_id, @@ -130,16 +129,6 @@ async def _messages_to_openai( }, } ) - if part.status in ("result", "error"): - tool_results.append( - { - "role": "tool", - "tool_call_id": part.tool_call_id, - "content": str(part.result) - if part.result is not None - else "", - } - ) entry: dict[str, Any] = {"role": "assistant"} if content: @@ -149,7 +138,19 @@ async def _messages_to_openai( if tool_calls: entry["tool_calls"] = tool_calls result.append(entry) - result.extend(tool_results) + + case "tool": + for part in msg.parts: + if isinstance(part, messages_.ToolResultPart): + result.append( + { + "role": "tool", + "tool_call_id": part.tool_call_id, + "content": str(part.result) + if part.result is not None + else "", + } + ) case "system": content_text = "".join( diff --git a/src/vercel_ai_sdk/types/__init__.py b/src/vercel_ai_sdk/types/__init__.py index 803f8b2a..49ecc00a 100644 --- a/src/vercel_ai_sdk/types/__init__.py +++ b/src/vercel_ai_sdk/types/__init__.py @@ -13,8 +13,9 @@ ReasoningPart, StructuredOutputPart, TextPart, + ToolCallPart, ToolDelta, - ToolPart, + ToolResultPart, Usage, generate_id, make_messages, @@ -30,9 +31,10 @@ "ReasoningPart", "StructuredOutputPart", "TextPart", + "ToolCallPart", "ToolDelta", - "ToolPart", "ToolLike", + "ToolResultPart", "ToolSchema", "Usage", "generate_id", diff --git a/src/vercel_ai_sdk/types/builders.py b/src/vercel_ai_sdk/types/builders.py index 0a4d9f62..7f48c54d 100644 --- a/src/vercel_ai_sdk/types/builders.py +++ b/src/vercel_ai_sdk/types/builders.py @@ -8,6 +8,8 @@ from __future__ import annotations +from typing import Any + from .messages import ( FilePart, HookPart, @@ -16,12 +18,14 @@ ReasoningPart, StructuredOutputPart, TextPart, - ToolPart, + ToolCallPart, + ToolResultPart, ) _PART_TYPES = ( TextPart, - ToolPart, + ToolCallPart, + ToolResultPart, ReasoningPart, HookPart, StructuredOutputPart, @@ -91,3 +95,30 @@ def thinking(text: str, *, signature: str | None = None) -> ReasoningPart: Useful for replaying conversation history that includes model reasoning. """ return ReasoningPart(text=text, signature=signature) + + +def tool_message(*parts: ToolResultPart) -> Message: + """Create a tool-result message from one or more :class:`ToolResultPart` objects. + + >>> ai.tool_message(ai.tool_result("tc-1", result=72, tool_name="weather")) + """ + return Message(role="tool", parts=list(parts)) + + +def tool_result( + tool_call_id: str, + *, + result: Any = None, + tool_name: str = "", + is_error: bool = False, +) -> ToolResultPart: + """Create a :class:`ToolResultPart`. + + >>> ai.tool_result("tc-1", result={"temp": 72}, tool_name="weather") + """ + return ToolResultPart( + tool_call_id=tool_call_id, + tool_name=tool_name, + result=result, + is_error=is_error, + ) diff --git a/src/vercel_ai_sdk/types/messages.py b/src/vercel_ai_sdk/types/messages.py index ef6279fa..3ec6ff7c 100644 --- a/src/vercel_ai_sdk/types/messages.py +++ b/src/vercel_ai_sdk/types/messages.py @@ -28,35 +28,41 @@ class TextPart(pydantic.BaseModel): delta: str | None = None # Current delta, None when not actively streaming -class ToolPart(pydantic.BaseModel): +class ToolCallPart(pydantic.BaseModel): + """A tool invocation requested by the LLM. + + Lives inside ``role="assistant"`` messages. The corresponding result + (if any) will appear as a :class:`ToolResultPart` in a separate + ``role="tool"`` message, linked by ``tool_call_id``. + """ + model_config = pydantic.ConfigDict(frozen=True) id: str = pydantic.Field(default_factory=generate_id) tool_call_id: str tool_name: str tool_args: str - status: Literal["pending", "result", "error"] = "pending" # Execution status - result: Any = None - type: Literal["tool"] = "tool" + type: Literal["tool_call"] = "tool_call" # Streaming state (for args streaming) state: PartState | None = None args_delta: str | None = None # Delta for tool_args - def with_result(self, result: Any) -> ToolPart: - """Return a copy with status='result' and the given result.""" - if self.status != "pending": - raise ValueError( - f"Tool call '{self.tool_call_id}' already has status '{self.status}'" - ) - return self.model_copy(update={"status": "result", "result": result}) - def with_error(self, message: str) -> ToolPart: - """Return a copy with status='error' and the error message.""" - if self.status != "pending": - raise ValueError( - f"Tool call '{self.tool_call_id}' already has status '{self.status}'" - ) - return self.model_copy(update={"status": "error", "result": message}) +class ToolResultPart(pydantic.BaseModel): + """The result of executing a tool call. + + Lives inside ``role="tool"`` messages. Back-references the + originating call via ``tool_call_id``. + """ + + model_config = pydantic.ConfigDict(frozen=True) + + id: str = pydantic.Field(default_factory=generate_id) + tool_call_id: str + tool_name: str + result: Any = None + is_error: bool = False + type: Literal["tool_result"] = "tool_result" class ReasoningPart(pydantic.BaseModel): @@ -202,7 +208,13 @@ def from_bytes( Part = Annotated[ - TextPart | ToolPart | ReasoningPart | HookPart | StructuredOutputPart | FilePart, + TextPart + | ToolCallPart + | ToolResultPart + | ReasoningPart + | HookPart + | StructuredOutputPart + | FilePart, pydantic.Field(discriminator="type"), ] @@ -270,7 +282,7 @@ class ToolDelta(pydantic.BaseModel): class Message(pydantic.BaseModel): model_config = pydantic.ConfigDict(frozen=True) - role: Literal["user", "assistant", "system"] + role: Literal["user", "assistant", "system", "tool"] parts: list[Part] id: str = pydantic.Field(default_factory=generate_id) label: str | None = None @@ -328,7 +340,7 @@ def is_done(self) -> bool: """Message is done when all parts are done (or have no streaming state).""" for part in self.parts: if ( - isinstance(part, (TextPart, ReasoningPart, ToolPart)) + isinstance(part, (TextPart, ReasoningPart, ToolCallPart)) and part.state == "streaming" ): return False @@ -355,7 +367,7 @@ def tool_deltas(self) -> list[ToolDelta]: """Get current tool deltas from parts.""" deltas = [] for part in self.parts: - if isinstance(part, ToolPart) and part.args_delta: + if isinstance(part, ToolCallPart) and part.args_delta: deltas.append( ToolDelta( tool_call_id=part.tool_call_id, @@ -403,9 +415,14 @@ def reasoning(self) -> str: return "" @property - def tool_calls(self) -> list[ToolPart]: - # TODO properly validate args? - return [part for part in self.parts if isinstance(part, ToolPart)] + def tool_calls(self) -> list[ToolCallPart]: + """All tool-call parts in this message.""" + return [part for part in self.parts if isinstance(part, ToolCallPart)] + + @property + def tool_results(self) -> list[ToolResultPart]: + """All tool-result parts in this message.""" + return [part for part in self.parts if isinstance(part, ToolResultPart)] def get_hook_part(self, hook_id: str | None = None) -> HookPart | None: """Find a HookPart by hook_id, or return the first HookPart if no id given.""" diff --git a/tests/adapters/ai_sdk_ui/test_adapter.py b/tests/adapters/ai_sdk_ui/test_adapter.py index 86e2a415..cc50c1e6 100644 --- a/tests/adapters/ai_sdk_ui/test_adapter.py +++ b/tests/adapters/ai_sdk_ui/test_adapter.py @@ -12,7 +12,7 @@ from vercel_ai_sdk.agents import hooks from vercel_ai_sdk.types import messages -from ...conftest import MOCK_MODEL, mock_llm, tool_msg +from ...conftest import MOCK_MODEL, mock_llm, tool_call_msg async def get_event_types(msgs: list[messages.Message]) -> list[str]: @@ -74,32 +74,28 @@ async def test_tool_roundtrip() -> None: Reference: process-ui-message-stream.test.ts "server-side tool roundtrip" """ msgs = [ - # Tool pending + # Tool call (assistant message) messages.Message( id="msg-1", role="assistant", parts=[ - messages.ToolPart( + messages.ToolCallPart( tool_call_id="tc-1", tool_name="get_weather", tool_args='{"city": "London"}', - status="pending", state="done", ), ], ), - # Tool result + # Tool result (tool message, pinned to same id for same step) messages.Message( id="msg-1", - role="assistant", + role="tool", parts=[ - messages.ToolPart( + messages.ToolResultPart( tool_call_id="tc-1", tool_name="get_weather", - tool_args='{"city": "London"}', - status="result", result={"weather": "sunny"}, - state="done", ), ], ), @@ -151,34 +147,29 @@ async def test_text_then_tool_then_text() -> None: ) ], ), - # Text done + tool pending + # Text done + tool call messages.Message( id="msg-1", role="assistant", parts=[ messages.TextPart(text="I'll check with the mothership.", state="done"), - messages.ToolPart( + messages.ToolCallPart( tool_call_id="tc-1", tool_name="talk_to_mothership", tool_args='{"question": "when?"}', - status="pending", state="done", ), ], ), - # Tool result + # Tool result (pinned to same id for same step) messages.Message( id="msg-1", - role="assistant", + role="tool", parts=[ - messages.TextPart(text="I'll check with the mothership.", state="done"), - messages.ToolPart( + messages.ToolResultPart( tool_call_id="tc-1", tool_name="talk_to_mothership", - tool_args='{"question": "when?"}', - status="result", result={"answer": "Soon."}, - state="done", ), ], ), @@ -268,11 +259,10 @@ async def test_runtime_tool_roundtrip() -> None: id="msg-1", role="assistant", parts=[ - messages.ToolPart( + messages.ToolCallPart( tool_call_id="tc-1", tool_name="get_weather", tool_args='{"city": "London"}', - status="pending", state="done", ), ], @@ -423,12 +413,13 @@ def test_ui_to_internal_two_turn_with_tool() -> None: # The single UI assistant message contains [text, tool(done), text] from # two stream_loop iterations. to_messages splits at the tool-result - # boundary so LLM adapters receive one message per iteration. - assert len(internal) == 4 + # boundary so LLM adapters receive one message per iteration: + # user, assistant (text + tool_call), tool (result), assistant (text), user + assert len(internal) == 5 assert internal[0].role == "user" assert internal[0].text == "when will the robots take over?" - # First iteration: text + tool call + # First iteration: text + tool call (assistant message) assert internal[1].role == "assistant" assert internal[1].text == ( "I'll check with the mothership about this important question." @@ -436,16 +427,19 @@ def test_ui_to_internal_two_turn_with_tool() -> None: assert len(internal[1].tool_calls) == 1 assert internal[1].tool_calls[0].tool_name == "talk_to_mothership" assert internal[1].tool_calls[0].tool_call_id == "toolu_01FiXNXhq1kHx4TegRjSaJyv" - assert internal[1].tool_calls[0].status == "result" - assert internal[1].tool_calls[0].result == {"value": "Soon."} + + # Tool result (separate tool message) + assert internal[2].role == "tool" + assert len(internal[2].tool_results) == 1 + assert internal[2].tool_results[0].result == {"value": "Soon."} # Second iteration: follow-up text - assert internal[2].role == "assistant" - assert internal[2].text == "The mothership has spoken: Soon." - assert len(internal[2].tool_calls) == 0 + assert internal[3].role == "assistant" + assert internal[3].text == "The mothership has spoken: Soon." + assert len(internal[3].tool_calls) == 0 - assert internal[3].role == "user" - assert internal[3].text == "this is a test run. can you remember the first turn?" + assert internal[4].role == "user" + assert internal[4].text == "this is a test run. can you remember the first turn?" def test_ui_tool_part_with_dict_input() -> None: @@ -470,7 +464,7 @@ def test_ui_tool_part_with_dict_input() -> None: tool_part = internal[0].tool_calls[0] assert tool_part.tool_name == "get_weather" assert tool_part.tool_args == '{"city": "London"}' - assert tool_part.status == "pending" # input-available maps to pending + # input-available means call is present but no result yet (no tool message) def test_ui_file_part_converted_to_core_file_part() -> None: @@ -541,16 +535,15 @@ async def test_tool_approval_hook_emits_approval_request() -> None: helper can find the tool part when the user responds to the approval. """ msgs = [ - # Tool pending (args complete, awaiting approval) + # Tool call (args complete, awaiting approval) messages.Message( id="msg-1", role="assistant", parts=[ - messages.ToolPart( + messages.ToolCallPart( tool_call_id="tc-1", tool_name="rm_rf", tool_args='{"path": "/"}', - status="pending", state="done", ), ], @@ -654,21 +647,26 @@ async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: last_msg = result.last_message assert last_msg is not None - async def approve_and_execute(tc: ai.ToolPart) -> ai.ToolPart: + async def approve_and_execute(tc: ai.ToolCallPart) -> ai.ToolResultPart: approval = await ai.ToolApproval.create( # type: ignore[attr-defined] f"approve_{tc.tool_call_id}", metadata={"tool_name": tc.tool_name}, ) if approval.granted: return await ai.execute_tool(tc, message=last_msg) - return tc.with_error("denied") + return ai.ToolResultPart( + tool_call_id=tc.tool_call_id, + tool_name=tc.tool_name, + result="denied", + is_error=True, + ) await asyncio.gather(*(approve_and_execute(tc) for tc in result.tool_calls)) mock_llm( [ [ - tool_msg( + tool_call_msg( tc_id="tc-1", name="dangerous_action", args='{"path": "/tmp"}', diff --git a/tests/agents/mcp/test_client.py b/tests/agents/mcp/test_client.py index a1220ac8..243df91d 100644 --- a/tests/agents/mcp/test_client.py +++ b/tests/agents/mcp/test_client.py @@ -10,7 +10,7 @@ from vercel_ai_sdk.agents.mcp.client import _mcp_tool_to_native from vercel_ai_sdk.agents.tools import _tool_registry, get_tool -from ...conftest import MOCK_MODEL, mock_llm, text_msg, tool_msg +from ...conftest import MOCK_MODEL, mock_llm, text_msg, tool_call_msg def _fake_mcp_tool( @@ -86,7 +86,9 @@ async def fake_fn(**kwargs: str) -> str: my_agent = ai.agent(model=MOCK_MODEL, tools=[native]) - call1 = [tool_msg(tc_id="tc-mcp-1", name="mcp_e2e_echo", args='{"text": "hello"}')] + call1 = [ + tool_call_msg(tc_id="tc-mcp-1", name="mcp_e2e_echo", args='{"text": "hello"}') + ] call2 = [text_msg("Done.", id="msg-2")] llm = mock_llm([call1, call2]) @@ -98,11 +100,9 @@ async def fake_fn(**kwargs: str) -> str: assert call_log[0] == {"text": "hello"} # Tool result is visible in messages - tool_results = [ - m for m in msgs if m.tool_calls and m.tool_calls[0].status == "result" - ] + tool_results = [m for m in msgs if m.role == "tool" and m.tool_results] assert len(tool_results) >= 1 - assert tool_results[0].tool_calls[0].result == "echoed: hello" + assert tool_results[0].tool_results[0].result == "echoed: hello" # LLM was called twice (tool call + final text) assert llm.call_count == 2 diff --git a/tests/agents/test_checkpoint.py b/tests/agents/test_checkpoint.py index 22fbd583..4c240017 100644 --- a/tests/agents/test_checkpoint.py +++ b/tests/agents/test_checkpoint.py @@ -9,7 +9,7 @@ import vercel_ai_sdk as ai from vercel_ai_sdk.agents.checkpoint import Checkpoint, HookEvent, StepEvent, ToolEvent -from ..conftest import MOCK_MODEL, mock_llm, text_msg, tool_msg +from ..conftest import MOCK_MODEL, mock_llm, text_msg, tool_call_msg @ai.hook @@ -66,7 +66,7 @@ async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> ai.StreamResult: ) return result - mock_llm([[tool_msg(tc_id="tc-1", name="counting_tool", args='{"x": 5}')]]) + mock_llm([[tool_call_msg(tc_id="tc-1", name="counting_tool", args='{"x": 5}')]]) result1 = my_agent.run(ai.make_messages(system="t", user="go")) [msg async for msg in result1] assert execution_count == 1 diff --git a/tests/agents/test_runtime.py b/tests/agents/test_runtime.py index 8cd2e8cd..4027d962 100644 --- a/tests/agents/test_runtime.py +++ b/tests/agents/test_runtime.py @@ -8,7 +8,7 @@ from vercel_ai_sdk.agents.runtime import Runtime from vercel_ai_sdk.types import messages -from ..conftest import MOCK_MODEL, mock_llm, text_msg, tool_msg +from ..conftest import MOCK_MODEL, mock_llm, text_msg, tool_call_msg # -- Tool definitions for tests -------------------------------------------- @@ -48,7 +48,7 @@ async def test_agent_tool_then_text() -> None: """Agent default loop calls tool, feeds result back, gets final text.""" my_agent = ai.agent(model=MOCK_MODEL, tools=[double]) - call1 = [tool_msg(tc_id="tc-1", name="double", args='{"x": 5}')] + call1 = [tool_call_msg(tc_id="tc-1", name="double", args='{"x": 5}')] call2 = [text_msg("The answer is 10.")] llm = mock_llm([call1, call2]) @@ -56,11 +56,9 @@ async def test_agent_tool_then_text() -> None: msgs = [m async for m in result] assert llm.call_count == 2 # Tool should have been executed: 5 * 2 = 10 - tool_results = [ - m for m in msgs if m.tool_calls and m.tool_calls[0].status == "result" - ] + tool_results = [m for m in msgs if m.role == "tool" and m.tool_results] assert len(tool_results) >= 1 - assert tool_results[0].tool_calls[0].result == 10 + assert tool_results[0].tool_results[0].result == 10 # -- Agent default loop: multiple tool calls in one message ---------------- @@ -75,18 +73,16 @@ async def test_agent_parallel_tools() -> None: id="msg-1", role="assistant", parts=[ - messages.ToolPart( + messages.ToolCallPart( tool_call_id="tc-1", tool_name="double", tool_args='{"x": 3}', - status="pending", state="done", ), - messages.ToolPart( + messages.ToolCallPart( tool_call_id="tc-2", tool_name="double", tool_args='{"x": 7}', - status="pending", state="done", ), ], @@ -98,11 +94,7 @@ async def test_agent_parallel_tools() -> None: msgs = [m async for m in result] assert llm.call_count == 2 # Both tools should have results - tool_result_msgs = [ - m - for m in msgs - if m.tool_calls and any(tc.status == "result" for tc in m.tool_calls) - ] + tool_result_msgs = [m for m in msgs if m.role == "tool" and m.tool_results] assert len(tool_result_msgs) >= 1 @@ -115,9 +107,9 @@ async def test_agent_multi_turn() -> None: my_agent = ai.agent(model=MOCK_MODEL, tools=[double, concat]) turn1 = [ - tool_msg(tc_id="tc-1", name="concat", args='{"a": "hello", "b": " world"}') + tool_call_msg(tc_id="tc-1", name="concat", args='{"a": "hello", "b": " world"}') ] - turn2 = [tool_msg(tc_id="tc-2", name="double", args='{"x": 3}', id="msg-2")] + turn2 = [tool_call_msg(tc_id="tc-2", name="double", args='{"x": 3}', id="msg-2")] turn3 = [text_msg("Done: hello world, 6", id="msg-3")] llm = mock_llm([turn1, turn2, turn3]) @@ -135,7 +127,7 @@ async def test_execute_tool_missing_raises() -> None: Wrapped in ExceptionGroup by TaskGroup. """ - tc = messages.ToolPart( + tc = messages.ToolCallPart( tool_call_id="tc-1", tool_name="nonexistent_tool_zzz", tool_args="{}" ) my_agent = ai.agent(model=MOCK_MODEL, tools=[]) @@ -179,7 +171,7 @@ async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: ) ) - call = [tool_msg(tc_id="tc-1", name="introspect", args='{"query": "test"}')] + call = [tool_call_msg(tc_id="tc-1", name="introspect", args='{"query": "test"}')] mock_llm([call]) result = my_agent.run(ai.make_messages(user="go")) [m async for m in result] @@ -191,8 +183,8 @@ async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: @pytest.mark.asyncio -async def test_execute_tool_returns_updated_part() -> None: - """execute_tool returns an updated ToolPart; the original is unchanged.""" +async def test_execute_tool_returns_result_part() -> None: + """execute_tool returns a ToolResultPart; the original ToolCallPart is unchanged.""" my_agent = ai.agent(model=MOCK_MODEL, tools=[double]) @my_agent.loop @@ -202,14 +194,14 @@ async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: msg = result.last_message assert msg is not None for tc in result.tool_calls: - updated_tc = await ai.execute_tool(tc, message=msg) - # Returned part has the result - assert updated_tc.status == "result" - assert updated_tc.result == 10 - # Original message is unchanged (immutable) - assert msg.tool_calls[0].status == "pending" - - call = [tool_msg(tc_id="tc-1", name="double", args='{"x": 5}')] + result_part = await ai.execute_tool(tc, message=msg) + # Returned part is a ToolResultPart with the result + assert not result_part.is_error + assert result_part.result == 10 + # Original message's tool calls are unchanged (immutable) + assert msg.tool_calls[0].tool_name == "double" + + call = [tool_call_msg(tc_id="tc-1", name="double", args='{"x": 5}')] mock_llm([call]) result = my_agent.run(ai.make_messages(user="go")) [m async for m in result] @@ -223,7 +215,7 @@ async def test_agent_checkpoint_records_tools() -> None: """Agent default loop's tool executions are recorded in the checkpoint.""" my_agent = ai.agent(model=MOCK_MODEL, tools=[double]) - call1 = [tool_msg(tc_id="tc-1", name="double", args='{"x": 4}')] + call1 = [tool_call_msg(tc_id="tc-1", name="double", args='{"x": 4}')] call2 = [text_msg("8", id="msg-2")] mock_llm([call1, call2]) diff --git a/tests/agents/test_streams.py b/tests/agents/test_streams.py index db7770ee..4bf546ef 100644 --- a/tests/agents/test_streams.py +++ b/tests/agents/test_streams.py @@ -40,10 +40,10 @@ def test_stream_result_tool_calls() -> None: id="m1", role="assistant", parts=[ - messages.ToolPart( + messages.ToolCallPart( tool_call_id="tc1", tool_name="t", tool_args="{}", state="done" ), - messages.ToolPart( + messages.ToolCallPart( tool_call_id="tc2", tool_name="u", tool_args="{}", state="done" ), ], diff --git a/tests/conftest.py b/tests/conftest.py index e949d1b9..953e8087 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import AsyncGenerator, Sequence -from typing import Any, Literal +from typing import Any import pydantic @@ -71,7 +71,7 @@ async def stream( streaming_.ReasoningEnd(block_id=bid, signature=part.signature) ) - elif isinstance(part, messages_.ToolPart): + elif isinstance(part, messages_.ToolCallPart): yield handler.handle_event( streaming_.ToolStart( tool_call_id=part.tool_call_id, @@ -116,21 +116,35 @@ def text_msg( return messages_.Message(id=id, role="assistant", parts=[part]) -def tool_msg( +def tool_call_msg( *, id: str = "msg-1", tc_id: str = "tc-1", name: str = "test_tool", args: str = "{}", - status: Literal["pending", "result", "error"] = "pending", - result: dict[str, object] | None = None, ) -> messages_.Message: - part: messages_.Part = messages_.ToolPart( + """Assistant message containing a tool call.""" + part: messages_.Part = messages_.ToolCallPart( tool_call_id=tc_id, tool_name=name, tool_args=args, - status=status, - result=result, state="done", ) return messages_.Message(id=id, role="assistant", parts=[part]) + + +def tool_result_msg( + *, + tc_id: str = "tc-1", + name: str = "test_tool", + result: Any = None, + is_error: bool = False, +) -> messages_.Message: + """Tool-result message.""" + part: messages_.Part = messages_.ToolResultPart( + tool_call_id=tc_id, + tool_name=name, + result=result, + is_error=is_error, + ) + return messages_.Message(role="tool", parts=[part]) diff --git a/tests/models/ai_gateway/test_protocol.py b/tests/models/ai_gateway/test_protocol.py index 8695b3ec..5cb25639 100644 --- a/tests/models/ai_gateway/test_protocol.py +++ b/tests/models/ai_gateway/test_protocol.py @@ -78,15 +78,23 @@ async def test_tool_call_with_result_produces_two_messages(self) -> None: messages.Message( role="assistant", parts=[ - messages.ToolPart( + messages.ToolCallPart( tool_call_id="tc-1", tool_name="get_weather", tool_args='{"city": "SF"}', - status="result", + ) + ], + ), + messages.Message( + role="tool", + parts=[ + messages.ToolResultPart( + tool_call_id="tc-1", + tool_name="get_weather", result={"temp": 72}, ) ], - ) + ), ] result = await stream_mod._messages_to_prompt(msgs) assert len(result) == 2 @@ -107,15 +115,24 @@ async def test_tool_error_result(self) -> None: messages.Message( role="assistant", parts=[ - messages.ToolPart( + messages.ToolCallPart( tool_call_id="tc-1", tool_name="get_weather", tool_args="{}", - status="error", + ) + ], + ), + messages.Message( + role="tool", + parts=[ + messages.ToolResultPart( + tool_call_id="tc-1", + tool_name="get_weather", result="Connection timeout", + is_error=True, ) ], - ) + ), ] result = await stream_mod._messages_to_prompt(msgs) tr = result[1]["content"][0] @@ -181,16 +198,16 @@ async def test_user_message_text_only_unchanged(self) -> None: ] async def test_pending_tool_call_no_tool_message(self) -> None: - """A pending tool call should NOT produce a tool-result message.""" + """A tool call without a corresponding tool-result message + should NOT produce a tool-result in the prompt.""" msgs = [ messages.Message( role="assistant", parts=[ - messages.ToolPart( + messages.ToolCallPart( tool_call_id="tc-1", tool_name="search", tool_args="{}", - status="pending", ) ], ) diff --git a/tests/models/ai_gateway/test_stream.py b/tests/models/ai_gateway/test_stream.py index 784dfac1..ed4e1241 100644 --- a/tests/models/ai_gateway/test_stream.py +++ b/tests/models/ai_gateway/test_stream.py @@ -339,16 +339,20 @@ def handler(req: httpx.Request) -> httpx.Response: text=_sse({"type": "finish", "finishReason": "stop", "usage": {}}), ) - tool_part = messages.ToolPart( + tool_call = messages.ToolCallPart( tool_call_id="tc-1", tool_name="search", tool_args='{"q": "weather"}', - status="result", + ) + tool_result = messages.ToolResultPart( + tool_call_id="tc-1", + tool_name="search", result={"temp": 72}, ) conversation = [ _user("What's the weather?"), - messages.Message(role="assistant", parts=[tool_part]), + messages.Message(role="assistant", parts=[tool_call]), + messages.Message(role="tool", parts=[tool_result]), _user("Thanks, and tomorrow?"), ] diff --git a/tests/models/core/test_streaming.py b/tests/models/core/test_streaming.py index 538d3a50..7eb92702 100644 --- a/tests/models/core/test_streaming.py +++ b/tests/models/core/test_streaming.py @@ -18,7 +18,7 @@ FilePart, ReasoningPart, TextPart, - ToolPart, + ToolCallPart, Usage, ) @@ -81,7 +81,7 @@ def test_tool_lifecycle() -> None: h.handle_event(ToolStart(tool_call_id="tc1", tool_name="get_weather")) m = h.handle_event(ToolArgsDelta(tool_call_id="tc1", delta='{"ci')) part = m.parts[0] - assert isinstance(part, ToolPart) + assert isinstance(part, ToolCallPart) assert part.tool_name == "get_weather" assert part.tool_args == '{"ci' assert part.state == "streaming" @@ -89,12 +89,12 @@ def test_tool_lifecycle() -> None: m = h.handle_event(ToolArgsDelta(tool_call_id="tc1", delta='ty":"London"}')) part = m.parts[0] - assert isinstance(part, ToolPart) + assert isinstance(part, ToolCallPart) assert part.tool_args == '{"city":"London"}' m = h.handle_event(ToolEnd(tool_call_id="tc1")) part = m.parts[0] - assert isinstance(part, ToolPart) + assert isinstance(part, ToolCallPart) assert part.state == "done" assert part.args_delta is None @@ -120,11 +120,11 @@ def test_reasoning_then_text_then_tool() -> None: assert len(m.parts) == 3 assert isinstance(m.parts[0], ReasoningPart) assert isinstance(m.parts[1], TextPart) - assert isinstance(m.parts[2], ToolPart) + assert isinstance(m.parts[2], ToolCallPart) assert all( p.state == "done" for p in m.parts - if isinstance(p, (TextPart, ToolPart, ReasoningPart)) + if isinstance(p, (TextPart, ToolCallPart, ReasoningPart)) ) @@ -136,7 +136,7 @@ def test_multiple_tool_calls() -> None: m = h.handle_event(ToolArgsDelta(tool_call_id="tc1", delta='{"path":"a.py"}')) # Both tools should be in parts - tool_parts = [p for p in m.parts if isinstance(p, ToolPart)] + tool_parts = [p for p in m.parts if isinstance(p, ToolCallPart)] assert len(tool_parts) == 2 # tc1 has args, tc2 is empty assert tool_parts[0].tool_args == '{"path":"a.py"}' @@ -148,7 +148,7 @@ def test_multiple_tool_calls() -> None: assert all( p.state == "done" for p in m.parts - if isinstance(p, (TextPart, ToolPart, ReasoningPart)) + if isinstance(p, (TextPart, ToolCallPart, ReasoningPart)) ) diff --git a/tests/telemetry/test_otel_handler.py b/tests/telemetry/test_otel_handler.py index a30dcf7c..88f54f58 100644 --- a/tests/telemetry/test_otel_handler.py +++ b/tests/telemetry/test_otel_handler.py @@ -12,7 +12,7 @@ import vercel_ai_sdk as ai from vercel_ai_sdk.telemetry.otel import OtelHandler -from ..conftest import MOCK_MODEL, mock_llm, text_msg, tool_msg +from ..conftest import MOCK_MODEL, mock_llm, text_msg, tool_call_msg @pytest.fixture @@ -68,7 +68,7 @@ async def test_tool_call_spans(spans: InMemorySpanExporter) -> None: mock_llm( [ - [tool_msg(tc_id="tc-1", name="double", args='{"x": 5}')], + [tool_call_msg(tc_id="tc-1", name="double", args='{"x": 5}')], [text_msg("10")], ] ) diff --git a/tests/telemetry/test_telemetry.py b/tests/telemetry/test_telemetry.py index b1b06eb3..04667f78 100644 --- a/tests/telemetry/test_telemetry.py +++ b/tests/telemetry/test_telemetry.py @@ -17,7 +17,7 @@ ToolCallStartEvent, ) -from ..conftest import MOCK_MODEL, mock_llm, text_msg, tool_msg +from ..conftest import MOCK_MODEL, mock_llm, text_msg, tool_call_msg # ── Recording handler ──────────────────────────────────────────── @@ -81,7 +81,7 @@ async def test_tool_call_events(handler: RecordingHandler) -> None: mock_llm( [ - [tool_msg(tc_id="tc-1", name="double", args='{"x": 5}')], + [tool_call_msg(tc_id="tc-1", name="double", args='{"x": 5}')], [text_msg("10")], ] ) diff --git a/tests/types/test_builders.py b/tests/types/test_builders.py index 00dc6557..5c6de420 100644 --- a/tests/types/test_builders.py +++ b/tests/types/test_builders.py @@ -13,7 +13,7 @@ FilePart, ReasoningPart, TextPart, - ToolPart, + ToolCallPart, ) # -- system_message -------------------------------------------------------- @@ -77,11 +77,11 @@ def test_assistant_message_with_thinking() -> None: assert isinstance(msg.parts[1], TextPart) -def test_assistant_message_with_tool_part() -> None: - tool = ToolPart(tool_call_id="tc-1", tool_name="test", tool_args="{}") +def test_assistant_message_with_tool_call_part() -> None: + tool = ToolCallPart(tool_call_id="tc-1", tool_name="test", tool_args="{}") msg = assistant_message("calling tool", tool) assert len(msg.parts) == 2 - assert isinstance(msg.parts[1], ToolPart) + assert isinstance(msg.parts[1], ToolCallPart) # -- file_part ------------------------------------------------------------- diff --git a/tests/types/test_messages.py b/tests/types/test_messages.py index 791c89ba..7c8ef3b1 100644 --- a/tests/types/test_messages.py +++ b/tests/types/test_messages.py @@ -1,4 +1,4 @@ -"""Message model: properties, immutability, part IDs, ToolPart.with_result/with_error, +"""Message model: properties, immutability, part IDs, ToolCallPart, ToolResultPart, Message.replace, make_messages, StructuredOutputPart, FilePart.""" import pydantic @@ -11,7 +11,8 @@ ReasoningPart, StructuredOutputPart, TextPart, - ToolPart, + ToolCallPart, + ToolResultPart, Usage, make_messages, ) @@ -34,7 +35,9 @@ def test_is_done_all_done() -> None: role="assistant", parts=[ TextPart(text="hello", state="done"), - ToolPart(tool_call_id="tc1", tool_name="t", tool_args="{}", state="done"), + ToolCallPart( + tool_call_id="tc1", tool_name="t", tool_args="{}", state="done" + ), ], ) assert m.is_done is True @@ -74,7 +77,7 @@ def test_text_empty_when_no_text_parts() -> None: m = Message( id="m1", role="assistant", - parts=[ToolPart(tool_call_id="tc1", tool_name="t", tool_args="{}")], + parts=[ToolCallPart(tool_call_id="tc1", tool_name="t", tool_args="{}")], ) assert m.text == "" @@ -119,7 +122,7 @@ def test_tool_deltas() -> None: id="m1", role="assistant", parts=[ - ToolPart( + ToolCallPart( tool_call_id="tc1", tool_name="search", tool_args='{"q":"te', @@ -143,8 +146,8 @@ def test_tool_calls() -> None: role="assistant", parts=[ TextPart(text="hi"), - ToolPart(tool_call_id="tc1", tool_name="a", tool_args="{}"), - ToolPart(tool_call_id="tc2", tool_name="b", tool_args="{}"), + ToolCallPart(tool_call_id="tc1", tool_name="a", tool_args="{}"), + ToolCallPart(tool_call_id="tc2", tool_name="b", tool_args="{}"), ], ) assert len(m.tool_calls) == 2 @@ -182,49 +185,34 @@ def test_get_hook_part_missing() -> None: def test_frozen_rejects_field_mutation() -> None: """Frozen models reject direct attribute assignment.""" - tp = ToolPart(tool_call_id="tc1", tool_name="t", tool_args="{}") + tc = ToolCallPart(tool_call_id="tc1", tool_name="t", tool_args="{}") with pytest.raises(pydantic.ValidationError): - tp.status = "result" # type: ignore[misc] + tc.tool_name = "other" # type: ignore[misc] m = Message(id="m1", role="assistant", parts=[TextPart(text="hi")]) with pytest.raises(pydantic.ValidationError): m.label = "test" # type: ignore[misc] -# -- ToolPart.with_result / with_error ------------------------------------ - - -def test_with_result() -> None: - tp = ToolPart(tool_call_id="tc1", tool_name="t", tool_args="{}") - assert tp.status == "pending" - updated = tp.with_result({"answer": 42}) - assert updated.status == "result" - assert updated.result == {"answer": 42} - assert updated.id == tp.id # id preserved - # Original unchanged - assert tp.status == "pending" - assert tp.result is None +# -- ToolResultPart -------------------------------------------------------- -def test_with_error() -> None: - tp = ToolPart(tool_call_id="tc1", tool_name="t", tool_args="{}") - updated = tp.with_error("Something went wrong") - assert updated.status == "error" - assert updated.result == "Something went wrong" - assert updated.id == tp.id # id preserved - # Original unchanged - assert tp.status == "pending" +def test_tool_result_part() -> None: + tr = ToolResultPart(tool_call_id="tc1", tool_name="t", result={"answer": 42}) + assert tr.tool_call_id == "tc1" + assert tr.result == {"answer": 42} + assert tr.is_error is False -def test_with_result_rejects_non_pending() -> None: - """with_result / with_error reject non-pending tool calls.""" - tp = ToolPart( - tool_call_id="tc1", tool_name="t", tool_args="{}", status="result", result=42 +def test_tool_result_part_error() -> None: + tr = ToolResultPart( + tool_call_id="tc1", + tool_name="t", + result="Something went wrong", + is_error=True, ) - with pytest.raises(ValueError, match="already has status"): - tp.with_result("new result") - with pytest.raises(ValueError, match="already has status"): - tp.with_error("oops") + assert tr.is_error is True + assert tr.result == "Something went wrong" # -- Part ids -------------------------------------------------------------- @@ -233,7 +221,7 @@ def test_with_result_rejects_non_pending() -> None: def test_parts_have_auto_generated_ids() -> None: """All parts get an auto-generated id.""" text = TextPart(text="hi") - tool = ToolPart(tool_call_id="tc1", tool_name="t", tool_args="{}") + tool = ToolCallPart(tool_call_id="tc1", tool_name="t", tool_args="{}") reasoning = ReasoningPart(text="thinking") assert text.id # non-empty assert tool.id @@ -252,20 +240,17 @@ def test_part_id_explicit() -> None: def test_replace() -> None: - tp = ToolPart(id="p1", tool_call_id="tc1", tool_name="t", tool_args="{}") + old_text = TextPart(id="p0", text="hello") m = Message( id="m1", role="assistant", - parts=[TextPart(id="p0", text="hi"), tp], + parts=[old_text, TextPart(id="p1", text="world")], ) - updated_tp = tp.with_result({"answer": 42}) - updated_m = m.replace(updated_tp) - tc = next(p for p in updated_m.parts if isinstance(p, ToolPart)) - assert tc.status == "result" - assert tc.result == {"answer": 42} + new_text = TextPart(id="p0", text="updated") + updated_m = m.replace(new_text) + assert updated_m.parts[0].text == "updated" # type: ignore[union-attr] # Original unchanged - orig_tc = next(p for p in m.parts if isinstance(p, ToolPart)) - assert orig_tc.status == "pending" + assert m.parts[0].text == "hello" # type: ignore[union-attr] def test_replace_missing_id() -> None: From 88e41741d80e43195a400cfdfce84c91299537fc Mon Sep 17 00:00:00 2001 From: Andrey Buzin Date: Wed, 8 Apr 2026 12:45:16 -0700 Subject: [PATCH 4/6] Port durability --- examples/agents3-temporal/activities.py | 101 +++ examples/agents3-temporal/direct.py | 129 +++ examples/agents3-temporal/main.py | 78 ++ examples/agents3-temporal/provider.py | 204 +++++ examples/agents3-temporal/pyproject.toml | 12 + examples/agents3-temporal/uv.lock | 987 +++++++++++++++++++++++ examples/agents3/custom_loop.py | 4 +- examples/samples/custom_loop.py | 2 +- src/vercel_ai_sdk/_durability.py | 37 + src/vercel_ai_sdk/agents/agent.py | 2 +- src/vercel_ai_sdk/agents3/__init__.py | 7 + src/vercel_ai_sdk/agents3/agent.py | 80 +- src/vercel_ai_sdk/agents3/checkpoint.py | 37 + src/vercel_ai_sdk/agents3/durability.py | 264 ++++++ src/vercel_ai_sdk/models/__init__.py | 52 +- 15 files changed, 1961 insertions(+), 35 deletions(-) create mode 100644 examples/agents3-temporal/activities.py create mode 100644 examples/agents3-temporal/direct.py create mode 100644 examples/agents3-temporal/main.py create mode 100644 examples/agents3-temporal/provider.py create mode 100644 examples/agents3-temporal/pyproject.toml create mode 100644 examples/agents3-temporal/uv.lock create mode 100644 src/vercel_ai_sdk/_durability.py create mode 100644 src/vercel_ai_sdk/agents3/checkpoint.py create mode 100644 src/vercel_ai_sdk/agents3/durability.py diff --git a/examples/agents3-temporal/activities.py b/examples/agents3-temporal/activities.py new file mode 100644 index 00000000..dbce3b1e --- /dev/null +++ b/examples/agents3-temporal/activities.py @@ -0,0 +1,101 @@ +"""Temporal activities — all real I/O lives here. + +Both examples (direct composition and provider) share these activities. +Each activity is a plain async function that does real I/O. +""" + +from __future__ import annotations + +import dataclasses +from typing import Any + +import temporalio.activity + +import vercel_ai_sdk as ai + +# ── Tool activities ────────────────────────────────────────────── + + +@temporalio.activity.defn(name="get_weather") +async def get_weather_activity(city: str) -> str: + return f"Sunny, 72F in {city}" + + +@temporalio.activity.defn(name="get_population") +async def get_population_activity(city: str) -> int: + return {"new york": 8_336_817, "los angeles": 3_979_576}.get( + city.lower(), 1_000_000 + ) + + +# ── Generic tool dispatch activity ────────────────────────────── +# +# Used by the provider example: the provider routes tool calls here +# instead of executing them inside the workflow. + +_TOOL_REGISTRY: dict[str, Any] = { + "get_weather": get_weather_activity, + "get_population": get_population_activity, +} + + +@dataclasses.dataclass +class ToolDispatchParams: + tool_name: str + tool_args: str # JSON string + + +@dataclasses.dataclass +class ToolDispatchResult: + result: Any + is_error: bool = False + + +@temporalio.activity.defn(name="tool_dispatch") +async def tool_dispatch_activity(params: ToolDispatchParams) -> ToolDispatchResult: + """Dispatch a tool call by name. Runs the real tool function.""" + import json + + fn = _TOOL_REGISTRY.get(params.tool_name) + if fn is None: + return ToolDispatchResult( + result=f"Unknown tool: {params.tool_name}", is_error=True + ) + + try: + kwargs = json.loads(params.tool_args) if params.tool_args else {} + result = await fn(**kwargs) + return ToolDispatchResult(result=result) + except Exception as exc: + return ToolDispatchResult(result=str(exc), is_error=True) + + +# ── LLM activity ──────────────────────────────────────────────── + + +@dataclasses.dataclass +class LLMCallParams: + messages: list[dict[str, Any]] + tool_schemas: list[dict[str, Any]] + + +@dataclasses.dataclass +class LLMCallResult: + message: dict[str, Any] # serialized ai.Message + + +@temporalio.activity.defn(name="llm_call") +async def llm_call_activity(params: LLMCallParams) -> LLMCallResult: + """Call the LLM, drain the stream, return the final message.""" + model = ai.Model( + id="anthropic/claude-sonnet-4-20250514", + adapter="ai-gateway-v3", + provider="ai-gateway", + ) + + messages = [ai.Message.model_validate(m) for m in params.messages] + tools = [ai.ToolSchema(return_type=None, **t) for t in params.tool_schemas] + + s = await ai.models.stream(model, messages, tools=tools) + result = await ai.models.buffer(s) + return LLMCallResult(message=result.model_dump()) diff --git a/examples/agents3-temporal/direct.py b/examples/agents3-temporal/direct.py new file mode 100644 index 00000000..6364977e --- /dev/null +++ b/examples/agents3-temporal/direct.py @@ -0,0 +1,129 @@ +"""Direct composition: a custom loop where every I/O call is a Temporal activity. + +No DurabilityProvider is involved. The user writes a plain ``@agent.loop`` +that replaces ``models.stream()`` and tool execution with Temporal +``execute_activity()`` calls. Temporal's event history provides durability. + +This is the "lego bricks" approach: the framework gives you ``Agent``, +``Context``, ``@tool`` (for schema extraction), and the message types. +You compose them with Temporal yourself. +""" + +from __future__ import annotations + +import asyncio +import datetime +from collections.abc import AsyncGenerator +from typing import Any + +import temporalio.common +import temporalio.workflow + +with temporalio.workflow.unsafe.imports_passed_through(): + import activities + + import vercel_ai_sdk as ai + from vercel_ai_sdk.agents3 import Context, agent, tool + + +# ── Tools ──────────────────────────────────────────────────────── +# +# Defined with @tool so the agent can extract JSON schemas for the +# LLM. The bodies are never called inside the workflow — execution +# goes through Temporal activities instead. + + +@tool +async def get_weather(city: str) -> str: + """Get current weather for a city.""" + raise RuntimeError("should not be called inside workflow") + + +@tool +async def get_population(city: str) -> int: + """Get population of a city.""" + raise RuntimeError("should not be called inside workflow") + + +# ── Agent with custom loop ─────────────────────────────────────── + +weather_agent = agent(tools=[get_weather, get_population]) + + +@weather_agent.loop +async def temporal_loop(context: Context) -> AsyncGenerator[ai.Message]: + """Agent loop where every I/O call is a durable Temporal activity.""" + tool_schemas = [ + { + "name": t.name, + "description": t.description, + "param_schema": t.param_schema, + } + for t in context.tools + ] + + while True: + # LLM call via activity. + result = await temporalio.workflow.execute_activity( + activities.llm_call_activity, + activities.LLMCallParams( + messages=[m.model_dump() for m in context.messages], + tool_schemas=tool_schemas, + ), + start_to_close_timeout=datetime.timedelta(minutes=5), + retry_policy=temporalio.common.RetryPolicy(maximum_attempts=3), + ) + msg = ai.Message.model_validate(result.message) + yield msg + + if not msg.tool_calls: + break + + # Tool calls via activities (parallel). + tool_call_parts = msg.tool_calls + + async def _run_tool(tc: Any) -> ai.ToolResultPart: + dispatch_result = await temporalio.workflow.execute_activity( + activities.tool_dispatch_activity, + activities.ToolDispatchParams( + tool_name=tc.tool_name, + tool_args=tc.tool_args, + ), + start_to_close_timeout=datetime.timedelta(minutes=2), + ) + return ai.ToolResultPart( + tool_call_id=tc.tool_call_id, + tool_name=tc.tool_name, + result=dispatch_result.result, + is_error=dispatch_result.is_error, + ) + + tasks = [asyncio.ensure_future(_run_tool(tc)) for tc in tool_call_parts] + results = await asyncio.gather(*tasks) + yield ai.tool_message(*results) + + +# ── Workflow ───────────────────────────────────────────────────── + + +@temporalio.workflow.defn +class DirectWorkflow: + @temporalio.workflow.run + async def run(self, user_query: str) -> str: + model = ai.Model( + id="anthropic/claude-sonnet-4-20250514", + adapter="ai-gateway-v3", + provider="ai-gateway", + ) + messages: list[ai.Message] = [ + ai.system_message( + "Answer questions using the weather and population tools." + ), + ai.user_message(user_query), + ] + + final_text = "" + async for msg in weather_agent.run(model, messages): + if msg.text: + final_text = msg.text + return final_text diff --git a/examples/agents3-temporal/main.py b/examples/agents3-temporal/main.py new file mode 100644 index 00000000..042e16f1 --- /dev/null +++ b/examples/agents3-temporal/main.py @@ -0,0 +1,78 @@ +"""Entry point — starts a Temporal worker and executes the agent workflow. + +Two examples in one project: + - ``direct`` — custom loop, each I/O call is a Temporal activity + - ``provider`` — default loop, DurabilityProvider routes I/O to activities + +Prerequisites: + 1. Temporal dev server: temporal server start-dev + 2. AI_GATEWAY_API_KEY environment variable set + +Usage: + uv run python main.py direct + uv run python main.py provider + uv run python main.py direct "What is the weather in Tokyo?" + uv run python main.py provider "Compare weather in NYC and LA" +""" + +from __future__ import annotations + +import asyncio +import sys +import uuid + +import activities +import direct +import provider +import temporalio.client +import temporalio.worker + +TASK_QUEUE = "agents3-durable" + + +async def main(mode: str, user_query: str) -> None: + temporal = await temporalio.client.Client.connect("localhost:7233") + + workflows = { + "direct": direct.DirectWorkflow, + "provider": provider.ProviderWorkflow, + } + workflow_cls = workflows[mode] + + async with temporalio.worker.Worker( + temporal, + task_queue=TASK_QUEUE, + workflows=[workflow_cls], + activities=[ + activities.llm_call_activity, + activities.get_weather_activity, + activities.get_population_activity, + activities.tool_dispatch_activity, + ], + ): + workflow_id = f"agents3-{mode}-{uuid.uuid4().hex[:8]}" + print(f"Mode: {mode}") + print(f"Workflow: {workflow_id}") + print(f"Query: {user_query}\n") + + result = await temporal.execute_workflow( + workflow_cls.run, + user_query, + id=workflow_id, + task_queue=TASK_QUEUE, + ) + print(result) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ("direct", "provider"): + print("Usage: python main.py [query]") + sys.exit(1) + + mode = sys.argv[1] + query = ( + sys.argv[2] + if len(sys.argv) > 2 + else "What's the weather and population of New York and Los Angeles?" + ) + asyncio.run(main(mode, query)) diff --git a/examples/agents3-temporal/provider.py b/examples/agents3-temporal/provider.py new file mode 100644 index 00000000..dee59bcd --- /dev/null +++ b/examples/agents3-temporal/provider.py @@ -0,0 +1,204 @@ +"""Provider-based durability: a DurabilityProvider backed by Temporal activities. + +The agent uses the **default loop** unchanged. A TemporalDurabilityProvider +is passed to ``agent.run(durability=...)``, and the framework auto-routes +``models.stream()`` and ``ToolCall.__call__()`` through the provider via +context var. + +The provider ignores the factory closures it receives (they can't be +serialized to a Temporal activity) and calls its own Temporal activities +instead. It tracks the conversation internally so it can serialize the +correct messages to the LLM activity at each turn. +""" + +from __future__ import annotations + +import datetime +from collections.abc import AsyncGenerator, Awaitable, Callable +from typing import Any + +import temporalio.common +import temporalio.workflow + +with temporalio.workflow.unsafe.imports_passed_through(): + import activities + + import vercel_ai_sdk as ai + from vercel_ai_sdk.agents3 import ( + Checkpoint, + agent, + tool, + ) + + +# ── Tools ──────────────────────────────────────────────────────── +# +# Defined with @tool for schema extraction. The default loop will +# try to call them via ToolCall.__call__(), but the provider intercepts +# and routes to Temporal activities instead. + + +@tool +async def get_weather(city: str) -> str: + """Get current weather for a city.""" + raise RuntimeError("should not be called inside workflow") + + +@tool +async def get_population(city: str) -> int: + """Get population of a city.""" + raise RuntimeError("should not be called inside workflow") + + +# ── Temporal durability provider ───────────────────────────────── + + +class _ActivityStreamResult: + """StreamResult-like wrapper around a buffered message from an activity.""" + + def __init__(self, message: ai.Message) -> None: + self._message = message + + def __aiter__(self) -> AsyncGenerator[ai.Message]: + return self._generate() + + async def _generate(self) -> AsyncGenerator[ai.Message]: + yield self._message + + @property + def tool_calls(self) -> list[ai.ToolCallPart]: + return self._message.tool_calls + + @property + def text(self) -> str: + return self._message.text + + @property + def usage(self) -> ai.Usage | None: + return self._message.usage + + @property + def output(self) -> Any: + return self._message.output + + +class TemporalDurabilityProvider: + """DurabilityProvider that routes LLM and tool I/O through Temporal activities. + + Temporal's event history provides durability — on replay, activities + return cached results automatically. No checkpoint needed on our side. + + The provider maintains its own copy of the conversation so it can + serialize the correct messages to the LLM activity at each step. + """ + + def __init__( + self, + initial_messages: list[ai.Message], + tool_schemas: list[dict[str, Any]], + ) -> None: + self._messages: list[ai.Message] = list(initial_messages) + self._tool_schemas = tool_schemas + + async def execute_stream( + self, + fn: Callable[[], Awaitable[Any]], + ) -> _ActivityStreamResult: + """Call the LLM via a Temporal activity instead of fn().""" + result = await temporalio.workflow.execute_activity( + activities.llm_call_activity, + activities.LLMCallParams( + messages=[m.model_dump() for m in self._messages], + tool_schemas=self._tool_schemas, + ), + start_to_close_timeout=datetime.timedelta(minutes=5), + retry_policy=temporalio.common.RetryPolicy(maximum_attempts=3), + ) + msg = ai.Message.model_validate(result.message) + # Track the assistant response. + self._messages.append(msg) + return _ActivityStreamResult(msg) + + async def execute_tool( + self, + fn: Callable[[], Awaitable[ai.ToolResultPart]], + *, + tool_call_id: str, + tool_name: str, + ) -> ai.ToolResultPart: + """Execute a tool via a Temporal activity instead of fn().""" + # Find tool_args from the last assistant message we recorded. + tool_args = "" + for msg in reversed(self._messages): + for tc in msg.tool_calls: + if tc.tool_call_id == tool_call_id: + tool_args = tc.tool_args + break + + result = await temporalio.workflow.execute_activity( + activities.tool_dispatch_activity, + activities.ToolDispatchParams( + tool_name=tool_name, + tool_args=tool_args, + ), + start_to_close_timeout=datetime.timedelta(minutes=2), + ) + + tool_result = ai.ToolResultPart( + tool_call_id=tool_call_id, + tool_name=tool_name, + result=result.result, + is_error=result.is_error, + ) + # Track the tool result. The default loop yields a tool_message + # after gathering all results, and _collect_messages appends it + # to context.messages. We mirror that here so our next LLM call + # has the full conversation. + self._messages.append(ai.tool_message(tool_result)) + return tool_result + + def checkpoint(self) -> Checkpoint: + """Temporal is the event store — no checkpoint needed.""" + return Checkpoint() + + +# ── Agent (uses default loop) ──────────────────────────────────── + +weather_agent = agent(tools=[get_weather, get_population]) + + +# ── Workflow ───────────────────────────────────────────────────── + + +@temporalio.workflow.defn +class ProviderWorkflow: + @temporalio.workflow.run + async def run(self, user_query: str) -> str: + model = ai.Model( + id="anthropic/claude-sonnet-4-20250514", + adapter="ai-gateway-v3", + provider="ai-gateway", + ) + messages: list[ai.Message] = [ + ai.system_message( + "Answer questions using the weather and population tools." + ), + ai.user_message(user_query), + ] + + tool_schemas = [ + { + "name": t.name, + "description": t.description, + "param_schema": t.param_schema, + } + for t in weather_agent._tools + ] + + provider = TemporalDurabilityProvider(messages, tool_schemas) + + final_text = "" + async for msg in weather_agent.run(model, messages, durability=provider): + if msg.text: + final_text = msg.text + return final_text diff --git a/examples/agents3-temporal/pyproject.toml b/examples/agents3-temporal/pyproject.toml new file mode 100644 index 00000000..8a5dc5eb --- /dev/null +++ b/examples/agents3-temporal/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "agents3-temporal" +version = "0.1.0" +description = "Durable agent execution with Temporal (agents3 API)" +requires-python = ">=3.12" +dependencies = [ + "vercel-ai-sdk", + "temporalio>=1.9.0", +] + +[tool.uv.sources] +vercel-ai-sdk = { path = "../..", editable = true } diff --git a/examples/agents3-temporal/uv.lock b/examples/agents3-temporal/uv.lock new file mode 100644 index 00000000..dbb06dae --- /dev/null +++ b/examples/agents3-temporal/uv.lock @@ -0,0 +1,987 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "agents3-temporal" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "temporalio" }, + { name = "vercel-ai-sdk" }, +] + +[package.metadata] +requires-dist = [ + { name = "temporalio", specifier = ">=1.9.0" }, + { name = "vercel-ai-sdk", editable = "../../" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.92.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/2d/fc5c5a369db977efbaa646d77ba42b38a6de4e95789884032b0e2e3fc834/anthropic-0.92.0.tar.gz", hash = "sha256:d1e792ed0692379452a1af6b266df495e973c3695cd0aace2a108b838393cbc4", size = 652420, upload-time = "2026-04-08T16:55:35.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/21/bf5b5ab10b6932c5c43eaa66b6e3f256de569cf0323d89f9cc281a0d0f39/anthropic-0.92.0-py3-none-any.whl", hash = "sha256:f92a4bd065d5cab90a96b65bb44e473bf7c6fe731a743cd156e9ad1d245c381e", size = 621195, upload-time = "2026-04-08T16:55:33.639Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "cbor2" +version = "5.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/cb/09939728be094d155b5d4ac262e39877875f5f7e36eea66beb359f647bd0/cbor2-5.9.0.tar.gz", hash = "sha256:85c7a46279ac8f226e1059275221e6b3d0e370d2bb6bd0500f9780781615bcea", size = 111231, upload-time = "2026-03-22T15:56:50.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/39/72d8a5a4b06565561ec28f4fcb41aff7bb77f51705c01f00b8254a2aca4f/cbor2-5.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f223dffb1bcdd2764665f04c1152943d9daa4bc124a576cd8dee1cad4264313", size = 71223, upload-time = "2026-03-22T15:56:13.68Z" }, + { url = "https://files.pythonhosted.org/packages/09/fd/7ddf3d3153b54c69c3be77172b8d9aa3a9d74f62a7fbde614d53eaeed9a4/cbor2-5.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae6c706ac1d85a0b3cb3395308fd0c4d55e3202b4760773675957e93cdff45fc", size = 287865, upload-time = "2026-03-22T15:56:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/db/9d/7ede2cc42f9bb4260492e7d29d2aab781eacbbcfb09d983de1e695077199/cbor2-5.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4cd43d8fc374b31643b2830910f28177a606a7bc84975a62675dd3f2e320fc7b", size = 288246, upload-time = "2026-03-22T15:56:16.113Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9d/588ebc7c5bc5843f609b05fe07be8575c7dec987735b0bbc908ac9c1264a/cbor2-5.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4aa07b392cc3d76fb31c08a46a226b58c320d1c172ff3073e864409ced7bc50f", size = 280214, upload-time = "2026-03-22T15:56:17.519Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a1/6fc8f4b15c6a27e7fbb7966c30c2b4b18c274a3221fa2f5e6235502d34bc/cbor2-5.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:971d425b3a23b75953d8853d5f9911bdeefa09d759ee3b5e6b07b5ff3cbd9073", size = 282162, upload-time = "2026-03-22T15:56:18.975Z" }, + { url = "https://files.pythonhosted.org/packages/cf/20/9a22cfe08be16ddfeef2542cf4eeed1b29f3f57ddbba0b42f7e0bb8331fd/cbor2-5.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:34a6cb15e6ab6a8eae94ad2041731cd3ef786af43a8df99f847969af5b902ee7", size = 70049, upload-time = "2026-03-22T15:56:20.502Z" }, + { url = "https://files.pythonhosted.org/packages/c6/9e/695f92d09006614034e25a9f5b10620f3b219f79c1bec3c37b7c6f27a7a9/cbor2-5.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:7d1ddc4541e7367ac58c2470cc0df847f7137167fe4f5729e2d3cc0b993d7da4", size = 65382, upload-time = "2026-03-22T15:56:21.526Z" }, + { url = "https://files.pythonhosted.org/packages/81/c5/4901e21a8afe9448fd947b11e8f383903207cd6dd0800e5f5a386838de5b/cbor2-5.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fbb06f34aa645b4deca66643bba3d400d20c15312d1fe88d429be60c1ab50f27", size = 71284, upload-time = "2026-03-22T15:56:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/1b/10/df643a381aebc3f05486de4813662bc58accb640fc3275cb276a75e89694/cbor2-5.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac684fe195c39821fca70d18afbf748f728aefbfbf88456018d299e559b8cae0", size = 287682, upload-time = "2026-03-22T15:56:24.024Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/8aa6b766059ae4a0ca1ec3ff96fe3823a69a7be880dba2e249f7fbe2700b/cbor2-5.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a54fbb32cb828c214f7f333a707e4aec61182e7efdc06ea5d9596d3ecee624a", size = 288009, upload-time = "2026-03-22T15:56:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/6236bc25c183a9cf7e8062e5dddf9eae9b0b14ebf14a58a69fe5a1e872c6/cbor2-5.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4753a6d1bc71054d9179557bc65740860f185095ccb401d46637fff028a5b3ec", size = 280437, upload-time = "2026-03-22T15:56:26.479Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0a/84328d23c3c68874ac6497edb9b1900579a1028efa54734df3f1762bbc15/cbor2-5.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:380e534482b843e43442b87d8777a7bf9bed20cb7526f89b780c3400f617304b", size = 282247, upload-time = "2026-03-22T15:56:28.644Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f6/89b4627e09d028c8e5fcaf7cb55f225c33ce6e037ec1844e65d02bcfa945/cbor2-5.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:dcf0f695873e5c94bd072d6af8698e72b8fb7f7a18f37e0bced1041b7111a6cf", size = 70089, upload-time = "2026-03-22T15:56:29.801Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7c/efadcd5f0102db692490e4e206988a2f98d39a09912090db497a2b800885/cbor2-5.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:f7c9751a9611601ab326d8f5837f01379195bbf06175fb4effeb552140e7c9e8", size = 65466, upload-time = "2026-03-22T15:56:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/08/7d/9ccc36d10ef96e6038e48046ebe1ce35a1e7814da0e1e204d09e6ef09b8d/cbor2-5.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23606d31ba1368bd1b6602e3020ee88fe9523ca80e8630faf6b2fc904fd84560", size = 71500, upload-time = "2026-03-22T15:56:31.876Z" }, + { url = "https://files.pythonhosted.org/packages/70/e1/a6cca2cc72e13f00030c6a649f57ae703eb2c620806ab70c40db8eab33fa/cbor2-5.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0322296b9d52f55880e300ba8ba09ecf644303b99b51138bbb1c0fb644fa7c3e", size = 286953, upload-time = "2026-03-22T15:56:33.292Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/24cd5ef488a957d90e016f200a3aad820e4c2f85edd61c9fe4523007a1ee/cbor2-5.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:422817286c1d0ce947fb2f7eca9212b39bddd7231e8b452e2d2cc52f15332dba", size = 285454, upload-time = "2026-03-22T15:56:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/a4/35/dca96818494c0ba47cdd73e8d809b27fa91f8fa0ce32a068a09237687454/cbor2-5.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9a4907e0c3035bb8836116854ed8e56d8aef23909d601fa59706320897ec2551", size = 279441, upload-time = "2026-03-22T15:56:35.888Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/d3362378b16e53cf7e535a3f5aed8476e2109068154e24e31981ef5bde9e/cbor2-5.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fb7afe77f8d269e42d7c4b515c6fd14f1ccc0625379fb6829b269f493d16eddd", size = 279673, upload-time = "2026-03-22T15:56:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/43/d1/3533a697e5842fff7c2f64912eb251f8dcab3a8b5d88e228d6eebc3b5021/cbor2-5.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:86baf870d4c0bfc6f79de3801f3860a84ab76d9c8b0abb7f081f2c14c38d79d3", size = 71940, upload-time = "2026-03-22T15:56:38.366Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e2/c6ba75f3fb25dfa15ab6999cc8709c821987e9ed8e375d7f58539261bcb9/cbor2-5.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:7221483fad0c63afa4244624d552abf89d7dfdbc5f5edfc56fc1ff2b4b818975", size = 67639, upload-time = "2026-03-22T15:56:39.39Z" }, + { url = "https://files.pythonhosted.org/packages/42/ff/b83492b096fbef26e9cb62c1a4bf2d3cef579ea7b33138c6c37c4ae66f67/cbor2-5.9.0-py3-none-any.whl", hash = "sha256:27695cbd70c90b8de5c4a284642c2836449b14e2c2e07e3ffe0744cb7669a01b", size = 24627, upload-time = "2026-03-22T15:56:48.847Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "jiter" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "mcp" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, +] + +[[package]] +name = "nexus-rpc" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/d5/cd1ffb202b76ebc1b33c1332a3416e55a39929006982adc2b1eb069aaa9b/nexus_rpc-1.4.0.tar.gz", hash = "sha256:3b8b373d4865671789cc43623e3dc0bcbf192562e40e13727e17f1c149050fba", size = 82367, upload-time = "2026-02-25T22:01:34.053Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/52/6327a5f4fda01207205038a106a99848a41c83e933cd23ea2cab3d2ebc6c/nexus_rpc-1.4.0-py3-none-any.whl", hash = "sha256:14c953d3519113f8ccec533a9efdb6b10c28afef75d11cdd6d422640c40b3a49", size = 29645, upload-time = "2026-02-25T22:01:33.122Z" }, +] + +[[package]] +name = "openai" +version = "2.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/15/52580c8fbc16d0675d516e8749806eda679b16de1e4434ea06fb6feaa610/openai-2.30.0.tar.gz", hash = "sha256:92f7661c990bda4b22a941806c83eabe4896c3094465030dd882a71abe80c885", size = 676084, upload-time = "2026-03-25T22:08:59.96Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/9e/5bfa2270f902d5b92ab7d41ce0475b8630572e71e349b2a4996d14bdda93/openai-2.30.0-py3-none-any.whl", hash = "sha256:9a5ae616888eb2748ec5e0c5b955a51592e0b201a11f4262db920f2a78c5231d", size = 1146656, upload-time = "2026-03-25T22:08:58.2Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/45/e23b5dc14ddb9918ae4a625379506b17b6f8fc56ca1d82db62462f59aea6/python_multipart-0.0.24.tar.gz", hash = "sha256:9574c97e1c026e00bc30340ef7c7d76739512ab4dfd428fec8c330fa6a5cc3c8", size = 37695, upload-time = "2026-04-05T20:49:13.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/73/89930efabd4da63cea44a3f438aeb753d600123570e6d6264e763617a9ce/python_multipart-0.0.24-py3-none-any.whl", hash = "sha256:9b110a98db707df01a53c194f0af075e736a770dc5058089650d70b4a182f950", size = 24420, upload-time = "2026-04-05T20:49:12.555Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[package]] +name = "temporalio" +version = "1.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nexus-rpc" }, + { name = "protobuf" }, + { name = "types-protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/9c/3782bab0bf11a40b550147c19a5d1a476c17405391751982408902d9f138/temporalio-1.25.0.tar.gz", hash = "sha256:a3bbec1dcc904f674402cfa4faae480fda490b1c53ea5440c1f1996c562016fb", size = 2152534, upload-time = "2026-04-08T18:53:55.388Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/e3/5676dd10d1164b6d6ca8752314054097b89c5da931e936af402a7b15236c/temporalio-1.25.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6dc1bc8e1773b1a833d86a7ede2dd90ef4e031ced5b748b59e7f09a5bf9b327d", size = 13943906, upload-time = "2026-04-08T18:53:30.022Z" }, + { url = "https://files.pythonhosted.org/packages/89/50/7cbf7f845973be986ec165348f72f7a409750842a04d554965a39be5cb4f/temporalio-1.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:3c8fdcf79ea5ae8ae2cf6f48072e4a86c3e0f4778f6a8a066c6ff1d336587db4", size = 13298719, upload-time = "2026-04-08T18:53:35.95Z" }, + { url = "https://files.pythonhosted.org/packages/d2/31/d474bab8535552add6ed289911bf1ffae5d7071823ece1069842190fcaed/temporalio-1.25.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:141f37aaafd7d090ba5c8776e4e9bc60df1fbc64b9f50c8f00e905a436588ddc", size = 13555435, upload-time = "2026-04-08T18:53:41.36Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c8/e7dc053d6107bf2a037a3c9fe7b86639a25dcb888bde0e1ca366901ee47f/temporalio-1.25.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7ca5bb80264976477d4dc7a839b3d22af8577ae92306526a061481db49bf92", size = 14052050, upload-time = "2026-04-08T18:53:46.44Z" }, + { url = "https://files.pythonhosted.org/packages/08/70/9340ed3a578321cbc153041d34834bb1ec3f1f3e3d9cded47cd1b7c3e403/temporalio-1.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:9411534279a2e64847231b6059c214bff4d57cfd1532bd09f333d0b1603daa7f", size = 14299684, upload-time = "2026-04-08T18:53:52.482Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "types-protobuf" +version = "6.32.1.20260221" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/e2/9aa4a3b2469508bd7b4e2ae11cbedaf419222a09a1b94daffcd5efca4023/types_protobuf-6.32.1.20260221.tar.gz", hash = "sha256:6d5fb060a616bfb076cbb61b4b3c3969f5fc8bec5810f9a2f7e648ee5cbcbf6e", size = 64408, upload-time = "2026-02-21T03:55:13.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/e8/1fd38926f9cf031188fbc5a96694203ea6f24b0e34bd64a225ec6f6291ba/types_protobuf-6.32.1.20260221-py3-none-any.whl", hash = "sha256:da7cdd947975964a93c30bfbcc2c6841ee646b318d3816b033adc2c4eb6448e4", size = 77956, upload-time = "2026-02-21T03:55:12.894Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, +] + +[[package]] +name = "vercel" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "cbor2" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "vercel-workers" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/2e/3ea1143db8fe7dcf2b03556cc050e60b18695f51fc3d8267132ef3e37ef8/vercel-0.5.3.tar.gz", hash = "sha256:0d19b41bf87b63563a3ab0e80c63dfa707572ffac96dbbede75eac41149d98ea", size = 98217, upload-time = "2026-03-27T23:59:59.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/4e/dad34da1211b6028173464f1a27f226249a0e81560507e7c3f277f91b821/vercel-0.5.3-py3-none-any.whl", hash = "sha256:79b4d19e10b7bc239b569725fcfa56bd40a2d565360a757e3d4674cacd972cd6", size = 118064, upload-time = "2026-03-27T23:59:58.423Z" }, +] + +[[package]] +name = "vercel-ai-sdk" +version = "0.0.1.dev9" +source = { editable = "../../" } +dependencies = [ + { name = "anthropic" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "openai" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "vercel" }, +] + +[package.metadata] +requires-dist = [ + { name = "anthropic", specifier = ">=0.83.0" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "mcp", specifier = ">=1.18.0" }, + { name = "openai", specifier = ">=2.14.0" }, + { name = "opentelemetry-api", specifier = ">=1.0" }, + { name = "pydantic", specifier = ">=2.12.5" }, + { name = "vercel", specifier = ">=0.3.8" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.11" }, + { name = "opentelemetry-sdk", specifier = ">=1.0" }, + { name = "pyright", specifier = ">=1.1.408" }, + { name = "pytest", specifier = ">=8.0" }, + { name = "pytest-asyncio", specifier = ">=0.24" }, + { name = "python-dotenv", specifier = ">=1.2.1" }, + { name = "rich", specifier = ">=14.2.0" }, + { name = "ruff", specifier = ">=0.8" }, +] + +[[package]] +name = "vercel-workers" +version = "0.0.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "python-dotenv" }, + { name = "vercel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/95/f89de37c534aa87395be0e68058af38d8b4608370efde783dfbaf1721021/vercel_workers-0.0.13.tar.gz", hash = "sha256:cbc6d698e381996fdf0b7f60dd814191b0f4657e9390fdfddc483b0fc0ef6fd4", size = 50591, upload-time = "2026-03-19T19:07:32.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/b1/276e3e196c660ca2b4b073afd69458572afa8467f3c1b8e426d5df5536ae/vercel_workers-0.0.13-py3-none-any.whl", hash = "sha256:fcd7e232239a62b400971179dfb8819f365297970eb1dbbf95e0f0f2fac4c031", size = 50542, upload-time = "2026-03-19T19:07:31.482Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] diff --git a/examples/agents3/custom_loop.py b/examples/agents3/custom_loop.py index 24020484..8035be7f 100644 --- a/examples/agents3/custom_loop.py +++ b/examples/agents3/custom_loop.py @@ -33,7 +33,9 @@ async def main() -> None: async def custom(context: Context) -> AsyncGenerator[ai.Message]: """Stream, execute tools with logging, repeat.""" while True: - s = ai.models.stream(context.model, context.messages, tools=context.tools) + s = await ai.models.stream( + context.model, context.messages, tools=context.tools + ) async for msg in s: yield msg diff --git a/examples/samples/custom_loop.py b/examples/samples/custom_loop.py index 0e87ec37..0d51961e 100644 --- a/examples/samples/custom_loop.py +++ b/examples/samples/custom_loop.py @@ -29,7 +29,7 @@ async def custom_stream_step( label: str | None = None, ) -> AsyncGenerator[ai.Message]: """Wraps models.stream to inject a label on every message.""" - async for msg in ai.models.stream(model, messages, tools=tools): + async for msg in await ai.models.stream(model, messages, tools=tools): msg.label = label yield msg diff --git a/src/vercel_ai_sdk/_durability.py b/src/vercel_ai_sdk/_durability.py new file mode 100644 index 00000000..fa041676 --- /dev/null +++ b/src/vercel_ai_sdk/_durability.py @@ -0,0 +1,37 @@ +"""Shared durability context var. + +Lives at the package root so that both ``models`` (lower-level) and +``agents3`` (higher-level) can import it without circular dependencies. +The actual ``DurabilityProvider`` protocol and implementations live in +``agents3.durability``; this module only holds the context var and a +thin accessor. +""" + +from __future__ import annotations + +import contextvars +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from .agents3 import durability + +# The context var stores Any at runtime to avoid importing the protocol +# at module level. ``agents3.durability`` provides the typed accessor. +_provider: contextvars.ContextVar[Any] = contextvars.ContextVar( + "durability_provider", default=None +) + + +def get_provider() -> durability.DurabilityProvider | None: + """Return the active durability provider, or ``None``.""" + return _provider.get(None) # type: ignore[no-any-return] + + +def set_provider(provider: Any) -> contextvars.Token[Any]: + """Set the active durability provider. Returns a reset token.""" + return _provider.set(provider) + + +def reset_provider(token: contextvars.Token[Any]) -> None: + """Reset the durability provider to its previous value.""" + _provider.reset(token) diff --git a/src/vercel_ai_sdk/agents/agent.py b/src/vercel_ai_sdk/agents/agent.py index eacca666..acaf51e7 100644 --- a/src/vercel_ai_sdk/agents/agent.py +++ b/src/vercel_ai_sdk/agents/agent.py @@ -61,7 +61,7 @@ async def stream_step( ``@stream``, so each call becomes a replayable step in the event log. """ - async for msg in models.stream( + async for msg in await models.stream( model, messages, tools=tools, output_type=output_type, **kwargs ): yield msg.model_copy(update={"label": label}) if label is not None else msg diff --git a/src/vercel_ai_sdk/agents3/__init__.py b/src/vercel_ai_sdk/agents3/__init__.py index f933bca1..35c3b33f 100644 --- a/src/vercel_ai_sdk/agents3/__init__.py +++ b/src/vercel_ai_sdk/agents3/__init__.py @@ -1,10 +1,17 @@ from .agent import Agent, Context, Tool, ToolCall, agent, tool +from .checkpoint import Checkpoint, StepEvent, ToolEvent +from .durability import DurabilityProvider, EventLogProvider __all__ = [ "Agent", + "Checkpoint", "Context", + "DurabilityProvider", + "EventLogProvider", + "StepEvent", "Tool", "ToolCall", + "ToolEvent", "agent", "tool", ] diff --git a/src/vercel_ai_sdk/agents3/agent.py b/src/vercel_ai_sdk/agents3/agent.py index b20382a0..616931e0 100644 --- a/src/vercel_ai_sdk/agents3/agent.py +++ b/src/vercel_ai_sdk/agents3/agent.py @@ -10,8 +10,11 @@ import pydantic +from .. import _durability as _dctx from .. import models, types from ..types import builders +from . import checkpoint as checkpoint_ +from . import durability as durability_ from . import runtime @@ -116,21 +119,36 @@ def args(self) -> str: return self._part.tool_args async def __call__(self) -> types.ToolResultPart: - """Execute the tool and return a :class:`ToolResultPart`.""" - try: - result = await self._tool(self._part.tool_args) - except Exception as exc: + """Execute the tool and return a :class:`ToolResultPart`. + + If a durability provider is active, the call is routed through + it for recording or replay. + """ + provider = _dctx.get_provider() + + async def _execute() -> types.ToolResultPart: + try: + result = await self._tool(self._part.tool_args) + except Exception as exc: + return types.ToolResultPart( + tool_call_id=self._part.tool_call_id, + tool_name=self._part.tool_name, + result=str(exc), + is_error=True, + ) return types.ToolResultPart( tool_call_id=self._part.tool_call_id, tool_name=self._part.tool_name, - result=str(exc), - is_error=True, + result=result, ) - return types.ToolResultPart( - tool_call_id=self._part.tool_call_id, - tool_name=self._part.tool_name, - result=result, - ) + + if provider is not None: + return await provider.execute_tool( + _execute, + tool_call_id=self.id, + tool_name=self.name, + ) + return await _execute() class Context(pydantic.BaseModel): @@ -156,7 +174,9 @@ def __call__(self, context: Context) -> AsyncGenerator[types.Message]: ... async def _default_loop(context: Context) -> AsyncGenerator[types.Message]: while True: - stream = models.stream(context.model, context.messages, tools=context.tools) + stream = await models.stream( + context.model, context.messages, tools=context.tools + ) async for message in stream: yield message @@ -211,14 +231,40 @@ def loop(self, fn: LoopFn) -> LoopFn: return fn async def run( - self, model: models.Model, messages: list[types.Message] + self, + model: models.Model, + messages: list[types.Message], + *, + durability: durability_.DurabilityProvider | None = None, + checkpoint: checkpoint_.Checkpoint | None = None, ) -> AsyncGenerator[types.Message]: - """Run the agent loop, yielding messages to the consumer.""" + """Run the agent loop, yielding messages to the consumer. + + Args: + model: The model to use for LLM calls. + messages: Initial conversation messages. + durability: Explicit durability provider. If ``None`` but + *checkpoint* is given, an :class:`EventLogProvider` is + created automatically. + checkpoint: Checkpoint to resume from. Implies eventlog + durability when no explicit *durability* is provided. + """ + # Convenience: checkpoint implies eventlog provider. + if checkpoint is not None and durability is None: + durability = durability_.EventLogProvider(checkpoint) + context = Context(model=model, messages=list(messages), tools=self._tools) - source = _collect_messages(self._loop_fn(context), context.messages) - async for message in runtime.run(source): - yield message + # Set the durability provider on the shared context var so that + # models.stream() and ToolCall.__call__() auto-detect it. + token = _dctx.set_provider(durability) if durability is not None else None + try: + source = _collect_messages(self._loop_fn(context), context.messages) + async for message in runtime.run(source): + yield message + finally: + if token is not None: + _dctx.reset_provider(token) def agent( diff --git a/src/vercel_ai_sdk/agents3/checkpoint.py b/src/vercel_ai_sdk/agents3/checkpoint.py new file mode 100644 index 00000000..5cd79839 --- /dev/null +++ b/src/vercel_ai_sdk/agents3/checkpoint.py @@ -0,0 +1,37 @@ +"""Checkpoint data model for durable agent execution. + +A Checkpoint is a serializable snapshot of all completed work in an agent +run. On re-entry, the durability provider replays cached results from the +checkpoint instead of re-executing LLM calls and tool invocations. +""" + +from __future__ import annotations + +from typing import Any + +import pydantic + +from ..types import messages as messages_ + + +class StepEvent(pydantic.BaseModel): + """A completed LLM stream step — stores the final done message.""" + + index: int + message: messages_.Message + + +class ToolEvent(pydantic.BaseModel): + """A completed tool execution.""" + + tool_call_id: str + tool_name: str + result: Any + status: str = "result" # "result" | "error" + + +class Checkpoint(pydantic.BaseModel): + """Serializable snapshot of all completed work in an agent run.""" + + steps: list[StepEvent] = [] + tools: list[ToolEvent] = [] diff --git a/src/vercel_ai_sdk/agents3/durability.py b/src/vercel_ai_sdk/agents3/durability.py new file mode 100644 index 00000000..93e50d35 --- /dev/null +++ b/src/vercel_ai_sdk/agents3/durability.py @@ -0,0 +1,264 @@ +"""Durability provider protocol and built-in EventLog implementation. + +A DurabilityProvider intercepts execution boundaries (LLM streams and +tool calls) to record results on fresh execution or replay cached results +on re-entry. The provider is set on a context var by ``Agent.run()`` and +auto-detected by ``models.stream()`` and ``ToolCall.__call__()``. + +Two ways to get durability: + +1. **Direct composability** — write a custom loop that wraps primitives in + your own SDK (Temporal activities, Restate handlers, etc.). No provider + needed. + +2. **Provider interface** — pass a ``DurabilityProvider`` to ``Agent.run()``. + The framework routes ``models.stream()`` and ``ToolCall.__call__()`` + through the provider automatically via context var. +""" + +from __future__ import annotations + +import logging +from collections.abc import AsyncGenerator, Awaitable, Callable +from typing import Any, Protocol + +from .. import _durability as _dctx +from ..types import messages as messages_ +from . import checkpoint as checkpoint_ + +logger = logging.getLogger(__name__) + +# Re-export the shared accessors for convenience. +get_provider = _dctx.get_provider +set_provider = _dctx.set_provider +reset_provider = _dctx.reset_provider + + +# ── Protocol ───────────────────────────────────────────────────── + + +class StreamResultLike(Protocol): + """Minimal interface that models.StreamResult satisfies.""" + + def __aiter__(self) -> AsyncGenerator[messages_.Message]: ... + + @property + def tool_calls(self) -> list[messages_.ToolCallPart]: ... + + @property + def text(self) -> str: ... + + @property + def usage(self) -> messages_.Usage | None: ... + + +class DurabilityProvider(Protocol): + """Abstract interface for durable execution of agent operations. + + Implementations intercept LLM streams and tool calls to either + record results (fresh execution) or replay cached results (re-entry). + """ + + async def execute_stream( + self, + fn: Callable[[], Awaitable[StreamResultLike]], + ) -> StreamResultLike: + """Wrap an LLM stream step. + + *fn* is an async factory that creates the real ``StreamResult``. + The provider may call it (fresh execution) or skip it and return + a ``StreamResult`` wrapping cached messages (replay). + + The provider manages its own step counter internally. + """ + ... + + async def execute_tool( + self, + fn: Callable[[], Awaitable[messages_.ToolResultPart]], + *, + tool_call_id: str, + tool_name: str, + ) -> messages_.ToolResultPart: + """Wrap a tool call. + + *fn* executes the real tool. The provider may call it or return + a cached ``ToolResultPart`` from the checkpoint. + """ + ... + + def checkpoint(self) -> checkpoint_.Checkpoint: + """Return a snapshot of all completed work.""" + ... + + +# ── EventLogProvider ───────────────────────────────────────────── + + +class _ReplayStreamResult: + """Lightweight StreamResult substitute for replayed steps. + + Yields the cached final message and exposes ``.tool_calls``, + ``.text``, and ``.usage`` just like ``models.StreamResult``. + """ + + def __init__(self, message: messages_.Message) -> None: + self._message = message + + def __aiter__(self) -> AsyncGenerator[messages_.Message]: + return self._generate() + + async def _generate(self) -> AsyncGenerator[messages_.Message]: + yield self._message + + @property + def tool_calls(self) -> list[messages_.ToolCallPart]: + return self._message.tool_calls + + @property + def text(self) -> str: + return self._message.text + + @property + def usage(self) -> messages_.Usage | None: + return self._message.usage + + @property + def output(self) -> Any: + return self._message.output + + +class _RecordingStreamResult: + """Wraps a real StreamResult, forwarding and recording.""" + + def __init__( + self, + inner: StreamResultLike, + *, + index: int, + steps: list[checkpoint_.StepEvent], + ) -> None: + self._inner = inner + self._index = index + self._steps = steps + self._final: messages_.Message | None = None + + def __aiter__(self) -> AsyncGenerator[messages_.Message]: + return self._generate() + + async def _generate(self) -> AsyncGenerator[messages_.Message]: + async for msg in self._inner: + self._final = msg + yield msg + + # Record the final done message. + if self._final is not None and self._final.is_done: + self._steps.append( + checkpoint_.StepEvent(index=self._index, message=self._final) + ) + + @property + def tool_calls(self) -> list[messages_.ToolCallPart]: + return self._final.tool_calls if self._final else [] + + @property + def text(self) -> str: + return self._final.text if self._final else "" + + @property + def usage(self) -> messages_.Usage | None: + return self._final.usage if self._final else None + + @property + def output(self) -> Any: + return self._final.output if self._final else None + + +class EventLogProvider: + """Built-in durability via event log replay. + + Records LLM stream results and tool call results during fresh + execution. On re-entry with a checkpoint, replays cached results + instead of re-executing. + + Usage:: + + provider = EventLogProvider(checkpoint) + async for msg in agent.run(model, messages, durability=provider): + ... + new_checkpoint = provider.checkpoint() + """ + + def __init__(self, cp: checkpoint_.Checkpoint | None = None) -> None: + self._checkpoint = cp or checkpoint_.Checkpoint() + self._step_cursor: int = 0 + self._tool_cache: dict[str, checkpoint_.ToolEvent] = { + t.tool_call_id: t for t in self._checkpoint.tools + } + + # New recordings + self._steps: list[checkpoint_.StepEvent] = [] + self._tools: list[checkpoint_.ToolEvent] = [] + + # ── Stream ──────────────────────────────────────────────── + + async def execute_stream( + self, + fn: Callable[[], Awaitable[StreamResultLike]], + ) -> StreamResultLike: + idx = self._step_cursor + self._step_cursor += 1 + + # Replay from checkpoint. + if idx < len(self._checkpoint.steps): + cached = self._checkpoint.steps[idx] + logger.info("Replaying stream step %d from checkpoint", idx) + return _ReplayStreamResult(cached.message) + + # Fresh execution — wrap to record. + return _RecordingStreamResult(await fn(), index=idx, steps=self._steps) + + # ── Tool ────────────────────────────────────────────────── + + async def execute_tool( + self, + fn: Callable[[], Awaitable[messages_.ToolResultPart]], + *, + tool_call_id: str, + tool_name: str, + ) -> messages_.ToolResultPart: + # Replay from checkpoint. + cached = self._tool_cache.get(tool_call_id) + if cached is not None: + logger.info( + "Replaying tool %s (call_id=%s) from checkpoint", + tool_name, + tool_call_id, + ) + return messages_.ToolResultPart( + tool_call_id=tool_call_id, + tool_name=tool_name, + result=cached.result, + is_error=cached.status == "error", + ) + + # Fresh execution. + result = await fn() + self._tools.append( + checkpoint_.ToolEvent( + tool_call_id=tool_call_id, + tool_name=tool_name, + result=result.result, + status="error" if result.is_error else "result", + ) + ) + return result + + # ── Checkpoint ──────────────────────────────────────────── + + def checkpoint(self) -> checkpoint_.Checkpoint: + """Build a full Checkpoint merging prior state + new recordings.""" + return checkpoint_.Checkpoint( + steps=list(self._checkpoint.steps) + self._steps, + tools=list(self._checkpoint.tools) + self._tools, + ) diff --git a/src/vercel_ai_sdk/models/__init__.py b/src/vercel_ai_sdk/models/__init__.py index db36eca8..a36236a3 100644 --- a/src/vercel_ai_sdk/models/__init__.py +++ b/src/vercel_ai_sdk/models/__init__.py @@ -13,18 +13,20 @@ msgs = [Message(role="user", parts=[TextPart(text="hello")])] # stream — auto-creates client from env vars - async for msg in models.stream(model, msgs): + s = await models.stream(model, msgs) + async for msg in s: print(msg.text_delta, end="") # buffer the whole response - result = await models.buffer(models.stream(model, msgs)) + result = await models.buffer(await models.stream(model, msgs)) print(result.text) # explicit client client = models.Client( base_url="https://custom.example.com/v3/ai", api_key="sk-...", ) - async for msg in models.stream(model, msgs, client=client): + s = await models.stream(model, msgs, client=client) + async for msg in s: ... """ @@ -152,7 +154,7 @@ def output(self) -> Any: return self._final.output if self._final else None -def stream( +async def stream( model: Model, messages: list[messages_.Message], *, @@ -166,19 +168,39 @@ def stream( Returns a :class:`StreamResult` that is async-iterable and collects the final ``Message``. After iteration, access ``.text``, ``.tool_calls``, ``.usage``, etc. + + If a :class:`~vercel_ai_sdk.agents3.durability.DurabilityProvider` is + active (set by ``Agent.run()``), the stream is routed through the + provider for recording or replay. """ - _ensure_adapters() - c = client or _auto_client(model) - adapter_fn = _stream_adapters.get(model.adapter) - if adapter_fn is None: - registered = ", ".join(sorted(_stream_adapters)) or "(none)" - raise KeyError( - f"No stream adapter registered for adapter={model.adapter!r}. " - f"Registered: {registered}" + # Lazy import to avoid circular dependency at module level. + from .._durability import get_provider + + provider = get_provider() + + async def _make_raw() -> StreamResult: + _ensure_adapters() + c = client or _auto_client(model) + adapter_fn = _stream_adapters.get(model.adapter) + if adapter_fn is None: + registered = ", ".join(sorted(_stream_adapters)) or "(none)" + raise KeyError( + f"No stream adapter registered for adapter={model.adapter!r}. " + f"Registered: {registered}" + ) + return StreamResult( + adapter_fn( + c, model, messages, tools=tools, output_type=output_type, **kwargs + ) ) - return StreamResult( - adapter_fn(c, model, messages, tools=tools, output_type=output_type, **kwargs) - ) + + if provider is not None: + # Provider returns a StreamResultLike — may be a replay or + # a recording wrapper. We return it typed as StreamResult; + # callers only use the shared protocol surface (.tool_calls, etc.). + return await provider.execute_stream(_make_raw) # type: ignore[return-value] + + return await _make_raw() async def generate( From 05b93690f159a7b0e30046bfff764c4eecc1440b Mon Sep 17 00:00:00 2001 From: Andrey Buzin Date: Wed, 8 Apr 2026 13:10:16 -0700 Subject: [PATCH 5/6] Port hooks --- examples/agents3/hooks.py | 102 ++++++++++ examples/agents3/hooks_serverless.py | 141 ++++++++++++++ src/vercel_ai_sdk/agents3/__init__.py | 8 +- src/vercel_ai_sdk/agents3/checkpoint.py | 17 ++ src/vercel_ai_sdk/agents3/durability.py | 24 +++ src/vercel_ai_sdk/agents3/hooks.py | 243 ++++++++++++++++++++++++ src/vercel_ai_sdk/agents3/runtime.py | 13 ++ src/vercel_ai_sdk/types/messages.py | 2 +- 8 files changed, 548 insertions(+), 2 deletions(-) create mode 100644 examples/agents3/hooks.py create mode 100644 examples/agents3/hooks_serverless.py create mode 100644 src/vercel_ai_sdk/agents3/hooks.py diff --git a/examples/agents3/hooks.py b/examples/agents3/hooks.py new file mode 100644 index 00000000..75215ef4 --- /dev/null +++ b/examples/agents3/hooks.py @@ -0,0 +1,102 @@ +"""Human-in-the-loop approval hooks. + +Demonstrates the function-based hook API: + - await hook("label", payload=Model) to suspend inside the loop + - resolve_hook("label", data) to unblock from outside + - Hook messages arrive with role="signal" +""" + +import asyncio +from collections.abc import AsyncGenerator + +import pydantic + +import vercel_ai_sdk as ai +from vercel_ai_sdk.agents3 import Context, agent, hook, resolve_hook, tool + + +class Approval(pydantic.BaseModel): + granted: bool + reason: str = "" + + +@tool +async def contact_mothership(query: str) -> str: + """Contact the mothership for important decisions.""" + return "Soon." + + +async def main() -> None: + model = ai.Model( + id="anthropic/claude-sonnet-4-20250514", + adapter="ai-gateway-v3", + provider="ai-gateway", + ) + + my_agent = agent( + system="Use the contact_mothership tool when asked about the future.", + tools=[contact_mothership], + ) + + @my_agent.loop + async def with_approval(context: Context) -> AsyncGenerator[ai.Message]: + while True: + s = await ai.models.stream( + context.model, context.messages, tools=context.tools + ) + async for msg in s: + yield msg + + tool_calls = context.resolve(s.tool_calls) + if not tool_calls: + return + + results = [] + for tc in tool_calls: + if tc.name == "contact_mothership": + # Suspends until resolved from outside the loop. + approval = await hook( + f"approve_{tc.id}", + payload=Approval, + metadata={"tool": tc.name, "args": tc.args}, + ) + if approval.granted: + results.append(await tc()) + else: + results.append( + ai.ToolResultPart( + tool_call_id=tc.id, + tool_name=tc.name, + result=f"Rejected: {approval.reason}", + is_error=True, + ) + ) + else: + results.append(await tc()) + + yield ai.tool_message(*results) + + async for msg in my_agent.run( + model, [ai.user_message("When will the robots take over?")] + ): + # Hook signals arrive with role="signal" + if msg.role == "signal": + hook_part = msg.get_hook_part() + if hook_part and hook_part.status == "pending": + answer = input(f"Approve {hook_part.hook_id}? [y/n] ") + resolve_hook( + hook_part.hook_id, + Approval( + granted=answer.strip().lower() in ("y", "yes"), + reason="operator decision", + ), + ) + continue + + if msg.text_delta: + print(msg.text_delta, end="", flush=True) + print() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/agents3/hooks_serverless.py b/examples/agents3/hooks_serverless.py new file mode 100644 index 00000000..eab88d17 --- /dev/null +++ b/examples/agents3/hooks_serverless.py @@ -0,0 +1,141 @@ +"""Serverless hook pattern: interrupt_loop=True. + +Demonstrates the serverless/stateless pattern where the agent run suspends +cleanly when a hook has no resolution, and resumes from a checkpoint on +re-entry with a pre-registered resolution. + +Flow: + 1. First run: hook fires, interrupt_loop=True cancels the future, + CancelledError is caught, run ends with a checkpoint. + 2. Second run: resolve_hook() pre-registers the answer, agent.run() + replays from checkpoint, hook finds the resolution immediately. +""" + +import asyncio +from collections.abc import AsyncGenerator + +import pydantic + +import vercel_ai_sdk as ai +from vercel_ai_sdk.agents3 import ( + Context, + EventLogProvider, + agent, + hook, + resolve_hook, + tool, +) + + +class Confirmation(pydantic.BaseModel): + approved: bool + reason: str = "" + + +@tool +async def delete_file(path: str) -> str: + """Delete a file at the given path.""" + return f"Deleted {path}" + + +async def main() -> None: + model = ai.Model( + id="anthropic/claude-sonnet-4-20250514", + adapter="ai-gateway-v3", + provider="ai-gateway", + ) + + my_agent = agent( + system="Delete files when asked. Always use the delete_file tool.", + tools=[delete_file], + ) + + @my_agent.loop + async def with_confirmation(context: Context) -> AsyncGenerator[ai.Message]: + while True: + s = await ai.models.stream( + context.model, context.messages, tools=context.tools + ) + async for msg in s: + yield msg + + tool_calls = context.resolve(s.tool_calls) + if not tool_calls: + return + + results = [] + for tc in tool_calls: + try: + confirmation = await hook( + f"confirm_{tc.id}", + payload=Confirmation, + metadata={"tool": tc.name, "args": tc.args}, + interrupt_loop=True, # serverless: cancel if unresolved + ) + except asyncio.CancelledError: + # No resolution available — bail out cleanly. + # The checkpoint captures everything up to this point. + return + + if confirmation.approved: + results.append(await tc()) + else: + results.append( + ai.ToolResultPart( + tool_call_id=tc.id, + tool_name=tc.name, + result=f"Rejected: {confirmation.reason}", + is_error=True, + ) + ) + + yield ai.tool_message(*results) + + # ── First run: no resolution, hook interrupts ───────────── + print("--- Run 1: hook fires, no resolution, run suspends ---") + pending_hook_labels: list[str] = [] + + durability = EventLogProvider() + async for msg in my_agent.run( + model, + [ai.user_message("Delete /tmp/old_logs.txt")], + durability=durability, + ): + if msg.role == "signal": + hook_part = msg.get_hook_part() + if hook_part and hook_part.status == "pending": + pending_hook_labels.append(hook_part.hook_id) + print( + f" Hook pending: {hook_part.hook_id}" + f" (metadata={hook_part.metadata})" + ) + elif msg.text_delta: + print(msg.text_delta, end="", flush=True) + + saved_checkpoint = durability.checkpoint() + print(f"\n Checkpoint saved: {len(saved_checkpoint.steps)} steps\n") + + # ── Second run: pre-register resolution, replay from checkpoint ── + print("--- Run 2: pre-register approval, resume from checkpoint ---") + # Resolve each pending hook captured from run 1. + # In a real app this would come from a user action (API call, button click). + for label in pending_hook_labels: + resolve_hook(label, Confirmation(approved=True, reason="user approved")) + + durability = EventLogProvider(saved_checkpoint) + async for msg in my_agent.run( + model, + [ai.user_message("Delete /tmp/old_logs.txt")], + durability=durability, + ): + if msg.role == "signal": + hook_part = msg.get_hook_part() + if hook_part: + print(f" Hook {hook_part.status}: {hook_part.hook_id}") + elif msg.text_delta: + print(msg.text_delta, end="", flush=True) + print() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/vercel_ai_sdk/agents3/__init__.py b/src/vercel_ai_sdk/agents3/__init__.py index 35c3b33f..6b440140 100644 --- a/src/vercel_ai_sdk/agents3/__init__.py +++ b/src/vercel_ai_sdk/agents3/__init__.py @@ -1,6 +1,7 @@ from .agent import Agent, Context, Tool, ToolCall, agent, tool -from .checkpoint import Checkpoint, StepEvent, ToolEvent +from .checkpoint import Checkpoint, HookEvent, PendingHookInfo, StepEvent, ToolEvent from .durability import DurabilityProvider, EventLogProvider +from .hooks import cancel_hook, hook, resolve_hook __all__ = [ "Agent", @@ -8,10 +9,15 @@ "Context", "DurabilityProvider", "EventLogProvider", + "HookEvent", + "PendingHookInfo", "StepEvent", "Tool", "ToolCall", "ToolEvent", "agent", + "cancel_hook", + "hook", + "resolve_hook", "tool", ] diff --git a/src/vercel_ai_sdk/agents3/checkpoint.py b/src/vercel_ai_sdk/agents3/checkpoint.py index 5cd79839..6cd0d96e 100644 --- a/src/vercel_ai_sdk/agents3/checkpoint.py +++ b/src/vercel_ai_sdk/agents3/checkpoint.py @@ -30,8 +30,25 @@ class ToolEvent(pydantic.BaseModel): status: str = "result" # "result" | "error" +class HookEvent(pydantic.BaseModel): + """A resolved hook.""" + + label: str + resolution: dict[str, Any] + + +class PendingHookInfo(pydantic.BaseModel): + """A hook that was suspended but not resolved when the run ended.""" + + label: str + payload_type: str # fully qualified name of the pydantic model + metadata: dict[str, Any] = {} + + class Checkpoint(pydantic.BaseModel): """Serializable snapshot of all completed work in an agent run.""" steps: list[StepEvent] = [] tools: list[ToolEvent] = [] + hooks: list[HookEvent] = [] + pending_hooks: list[PendingHookInfo] = [] diff --git a/src/vercel_ai_sdk/agents3/durability.py b/src/vercel_ai_sdk/agents3/durability.py index 93e50d35..265c3eb9 100644 --- a/src/vercel_ai_sdk/agents3/durability.py +++ b/src/vercel_ai_sdk/agents3/durability.py @@ -87,6 +87,14 @@ async def execute_tool( """ ... + def get_hook_resolution(self, label: str) -> dict[str, Any] | None: + """Return a cached hook resolution, or ``None`` if not cached.""" + ... + + def record_hook(self, label: str, resolution: dict[str, Any]) -> None: + """Record a hook resolution for checkpoint.""" + ... + def checkpoint(self) -> checkpoint_.Checkpoint: """Return a snapshot of all completed work.""" ... @@ -195,10 +203,14 @@ def __init__(self, cp: checkpoint_.Checkpoint | None = None) -> None: self._tool_cache: dict[str, checkpoint_.ToolEvent] = { t.tool_call_id: t for t in self._checkpoint.tools } + self._hook_cache: dict[str, dict[str, Any]] = { + h.label: h.resolution for h in self._checkpoint.hooks + } # New recordings self._steps: list[checkpoint_.StepEvent] = [] self._tools: list[checkpoint_.ToolEvent] = [] + self._hooks: list[checkpoint_.HookEvent] = [] # ── Stream ──────────────────────────────────────────────── @@ -254,6 +266,17 @@ async def execute_tool( ) return result + # ── Hook ────────────────────────────────────────────────── + + def get_hook_resolution(self, label: str) -> dict[str, Any] | None: + cached = self._hook_cache.get(label) + if cached is not None: + logger.info("Resolving hook '%s' from checkpoint", label) + return cached + + def record_hook(self, label: str, resolution: dict[str, Any]) -> None: + self._hooks.append(checkpoint_.HookEvent(label=label, resolution=resolution)) + # ── Checkpoint ──────────────────────────────────────────── def checkpoint(self) -> checkpoint_.Checkpoint: @@ -261,4 +284,5 @@ def checkpoint(self) -> checkpoint_.Checkpoint: return checkpoint_.Checkpoint( steps=list(self._checkpoint.steps) + self._steps, tools=list(self._checkpoint.tools) + self._tools, + hooks=list(self._checkpoint.hooks) + self._hooks, ) diff --git a/src/vercel_ai_sdk/agents3/hooks.py b/src/vercel_ai_sdk/agents3/hooks.py new file mode 100644 index 00000000..09c92911 --- /dev/null +++ b/src/vercel_ai_sdk/agents3/hooks.py @@ -0,0 +1,243 @@ +"""Hooks: suspension points that require external input to continue. + +Usage inside an agent loop:: + + result = await hook("approve_delete", payload=ToolApproval, metadata={"tool": "rm"}) + if result.granted: + ... + +Resolution from outside the loop:: + + resolve_hook("approve_delete", {"granted": True}) + +Cancellation:: + + await cancel_hook("approve_delete", reason="denied") + +Behavior depends on ``interrupt_loop``: + +interrupt_loop=False (default, long-running): the await blocks until +resolve_hook() is called from outside (e.g. websocket handler, API endpoint). + +interrupt_loop=True (serverless): if no resolution is available, the +hook's future is cancelled. The branch receives CancelledError and dies +cleanly. On re-entry, call resolve_hook() before agent.run() to +pre-register the resolution, then pass checkpoint= to replay. +""" + +from __future__ import annotations + +import asyncio +from typing import Any + +import pydantic + +from .. import _durability as _dctx +from ..types import messages as messages_ +from . import runtime as runtime_ + +# --------------------------------------------------------------------------- +# Module-level hook registries +# +# _live_hooks: +# Populated by hook() when it suspends inside a running agent. +# Maps hook label -> (future, metadata dict, Runtime). +# Consumed by resolve_hook() / cancel_hook() to unblock the awaiting +# coroutine. Entries are removed when the hook resolves, cancels, or +# the run completes. +# +# _pending_resolutions: +# Populated by resolve_hook() when no live hook exists yet (serverless +# re-entry: the user calls resolve_hook() *before* agent.run() replays). +# Maps hook label -> (payload type, validated resolution dict). +# Consumed by hook() at the start of execution — if a pre-registered +# resolution exists for the label, the hook returns immediately without +# suspending. Entries are removed on consumption. +# --------------------------------------------------------------------------- + +_live_hooks: dict[ + str, tuple[asyncio.Future[dict[str, Any]], dict[str, Any], runtime_.Runtime] +] = {} + +_pending_resolutions: dict[str, dict[str, Any]] = {} + + +def cleanup_run(labels: set[str]) -> None: + """Remove all registry entries associated with a finished run.""" + for label in labels: + _live_hooks.pop(label, None) + _pending_resolutions.pop(label, None) + + +async def hook[T: pydantic.BaseModel]( + label: str, + *, + payload: type[T], + metadata: dict[str, Any] | None = None, + interrupt_loop: bool = False, +) -> T: + """Create a hook suspension point and await its resolution. + + Args: + label: Unique identifier for this hook instance. + payload: Pydantic model class — the resolution data must validate + against this type. The return value is a validated instance. + metadata: Arbitrary metadata surfaced in the pending signal message + and checkpoint. Useful for UI rendering (e.g. which tool needs + approval, what arguments it received). + interrupt_loop: When ``True`` (serverless mode), the hook's future + is cancelled if no resolution is available, causing + ``CancelledError`` in the awaiting coroutine. When ``False`` + (long-running mode), the future is held until resolved + externally. + """ + rt = runtime_.get_runtime() + hook_metadata = metadata or {} + + provider = _dctx.get_provider() + + # Path 1: pre-registered resolution (serverless re-entry). + pre_registered = _pending_resolutions.pop(label, None) + if pre_registered is not None: + if provider is not None: + provider.record_hook(label, pre_registered) + return payload(**pre_registered) + + # Path 2: cached resolution from checkpoint (durability replay). + if provider is not None: + cached = provider.get_hook_resolution(label) + if cached is not None: + provider.record_hook(label, cached) + return payload(**cached) + + # Path 3: no resolution available — suspend. + future: asyncio.Future[dict[str, Any]] = asyncio.Future() + + _live_hooks[label] = (future, hook_metadata, rt) + rt.track_hook_label(label) + + # Emit pending signal message. + await rt.put_message( + messages_.Message( + role="signal", + parts=[ + messages_.HookPart( + hook_id=label, + hook_type=payload.__name__, + status="pending", + metadata=hook_metadata, + ) + ], + ) + ) + + if interrupt_loop: + # Yield control so the consumer can see the pending message, + # then cancel — the caller catches CancelledError. + await asyncio.sleep(0) + if not future.done(): + future.cancel() + + # Await resolution — may be resolved externally or cancelled. + resolution = await future + + # Clean up live registry. + _live_hooks.pop(label, None) + + # Record for checkpoint. + if provider is not None: + provider.record_hook(label, resolution) + + # Emit resolved signal message. + await rt.put_message( + messages_.Message( + role="signal", + parts=[ + messages_.HookPart( + hook_id=label, + hook_type=payload.__name__, + status="resolved", + metadata=hook_metadata, + resolution=resolution, + ) + ], + ) + ) + + return payload(**resolution) + + +def resolve_hook( + label: str, + data: pydantic.BaseModel | dict[str, Any], + *, + payload: type[pydantic.BaseModel] | None = None, +) -> None: + """Resolve a hook by label. + + Works in two modes: + + 1. **Live hook exists** (long-running): validates data (if ``payload`` + type is provided), resolves the future immediately, unblocking the + awaiting coroutine. + + 2. **No live hook yet** (serverless re-entry): stashes the resolution + in the pre-registration registry. When ``hook()`` executes during + replay, it finds the pre-registered value and returns without + suspending. + + Args: + label: The hook label to resolve. + data: Resolution data — a dict or pydantic model instance. + payload: Optional pydantic model class for validation. When + omitted and *data* is a model instance, its type is used. + """ + # Normalize to dict. + if isinstance(data, pydantic.BaseModel): + resolution = data.model_dump() + elif isinstance(data, dict): + if payload is not None: + # Validate against the payload type. + validated = payload(**data) + resolution = validated.model_dump() + else: + resolution = data + else: + raise TypeError(f"Expected dict or pydantic model, got {type(data).__name__}") + + # Path 1: live hook — resolve the future directly. + if label in _live_hooks: + future, _, _rt = _live_hooks[label] + future.set_result(resolution) + return + + # Path 2: no live hook — pre-register for later consumption. + _pending_resolutions[label] = resolution + + +async def cancel_hook(label: str, *, reason: str | None = None) -> None: + """Cancel a pending hook. + + Only works for live hooks (long-running mode). Raises ValueError + if the hook is not currently pending. + """ + if label not in _live_hooks: + raise ValueError(f"No pending hook with label: {label!r}") + + future, hook_metadata, rt = _live_hooks.pop(label) + future.cancel(reason) + + # Emit cancelled signal message. + await rt.put_message( + messages_.Message( + role="signal", + parts=[ + messages_.HookPart( + hook_id=label, + hook_type="", # not available at cancel site + status="cancelled", + metadata=hook_metadata, + ) + ], + ) + ) diff --git a/src/vercel_ai_sdk/agents3/runtime.py b/src/vercel_ai_sdk/agents3/runtime.py index 71c26ec6..484f3ed8 100644 --- a/src/vercel_ai_sdk/agents3/runtime.py +++ b/src/vercel_ai_sdk/agents3/runtime.py @@ -21,6 +21,7 @@ def __init__(self) -> None: self._message_queue: asyncio.Queue[types.Message | Runtime._Sentinel] = ( asyncio.Queue() ) + self._hook_labels: set[str] = set() async def put_message(self, message: types.Message) -> None: await self._message_queue.put(message) @@ -28,6 +29,17 @@ async def put_message(self, message: types.Message) -> None: async def signal_done(self) -> None: await self._message_queue.put(self._SENTINEL) + def track_hook_label(self, label: str) -> None: + """Register a hook label for cleanup when the run ends.""" + self._hook_labels.add(label) + + def cleanup_hooks(self) -> None: + """Remove all hook registry entries for this run.""" + from . import hooks as hooks_ + + hooks_.cleanup_run(self._hook_labels) + self._hook_labels.clear() + _runtime: contextvars.ContextVar[Runtime] = contextvars.ContextVar("runtime") @@ -67,4 +79,5 @@ async def _drain() -> None: yield item finally: + runtime.cleanup_hooks() _runtime.reset(token) diff --git a/src/vercel_ai_sdk/types/messages.py b/src/vercel_ai_sdk/types/messages.py index 3ec6ff7c..d3c7450e 100644 --- a/src/vercel_ai_sdk/types/messages.py +++ b/src/vercel_ai_sdk/types/messages.py @@ -282,7 +282,7 @@ class ToolDelta(pydantic.BaseModel): class Message(pydantic.BaseModel): model_config = pydantic.ConfigDict(frozen=True) - role: Literal["user", "assistant", "system", "tool"] + role: Literal["user", "assistant", "system", "tool", "signal"] parts: list[Part] id: str = pydantic.Field(default_factory=generate_id) label: str | None = None From df89f5c8b20f105cc40ee2430206eefa96e0b33a Mon Sep 17 00:00:00 2001 From: Andrey Buzin Date: Wed, 8 Apr 2026 13:47:15 -0700 Subject: [PATCH 6/6] Migrate to the new module --- .../activities.py | 0 .../direct.py | 2 +- .../main.py | 4 +- .../provider.py | 2 +- .../pyproject.toml | 2 +- .../uv.lock | 0 examples/{agents3 => agents}/custom_loop.py | 2 +- examples/{agents3 => agents}/hooks.py | 2 +- .../{agents3 => agents}/hooks_serverless.py | 2 +- examples/{agents3 => agents}/nested_agents.py | 2 +- examples/{agents3 => agents}/simple.py | 2 +- examples/agents3-temporal/pyproject.toml | 12 - examples/temporal-durable/.gitignore | 10 - examples/temporal-durable/.python-version | 1 - examples/temporal-durable/README.md | 61 -- examples/temporal-durable/activities.py | 55 -- examples/temporal-durable/main.py | 58 -- examples/temporal-durable/uv.lock | 900 ------------------ examples/temporal-durable/workflow.py | 150 --- src/vercel_ai_sdk/__init__.py | 50 +- src/vercel_ai_sdk/_durability.py | 8 +- .../adapters/ai_sdk_ui/adapter.py | 5 +- src/vercel_ai_sdk/agents/__init__.py | 77 +- src/vercel_ai_sdk/agents/agent.py | 461 +++++---- src/vercel_ai_sdk/agents/checkpoint.py | 20 +- src/vercel_ai_sdk/agents/context.py | 206 ---- .../{agents3 => agents}/durability.py | 0 src/vercel_ai_sdk/agents/hooks.py | 399 ++++---- src/vercel_ai_sdk/agents/mcp/__init__.py | 3 +- src/vercel_ai_sdk/agents/mcp/client.py | 74 +- src/vercel_ai_sdk/agents/runtime.py | 634 ++---------- src/vercel_ai_sdk/agents/streams.py | 105 -- src/vercel_ai_sdk/agents/tools.py | 136 --- src/vercel_ai_sdk/agents3/__init__.py | 23 - src/vercel_ai_sdk/agents3/agent.py | 276 ------ src/vercel_ai_sdk/agents3/checkpoint.py | 54 -- src/vercel_ai_sdk/agents3/hooks.py | 243 ----- src/vercel_ai_sdk/agents3/runtime.py | 83 -- src/vercel_ai_sdk/models/__init__.py | 2 +- src/vercel_ai_sdk/telemetry/events.py | 5 +- src/vercel_ai_sdk/telemetry/otel.py | 9 +- src/vercel_ai_sdk/types/tools.py | 2 +- tests/adapters/ai_sdk_ui/test_adapter.py | 70 +- tests/agents/mcp/test_client.py | 37 +- tests/agents/test_checkpoint.py | 213 ++--- tests/agents/test_hooks.py | 143 +-- tests/agents/test_runtime.py | 145 +-- tests/agents/test_streams.py | 113 --- tests/agents/test_tools.py | 77 +- tests/telemetry/test_otel_handler.py | 12 +- tests/telemetry/test_telemetry.py | 50 +- 51 files changed, 936 insertions(+), 4066 deletions(-) rename examples/{agents3-temporal => agents-temporal}/activities.py (100%) rename examples/{agents3-temporal => agents-temporal}/direct.py (98%) rename examples/{agents3-temporal => agents-temporal}/main.py (95%) rename examples/{agents3-temporal => agents-temporal}/provider.py (99%) rename examples/{temporal-durable => agents-temporal}/pyproject.toml (90%) rename examples/{agents3-temporal => agents-temporal}/uv.lock (100%) rename examples/{agents3 => agents}/custom_loop.py (97%) rename examples/{agents3 => agents}/hooks.py (97%) rename examples/{agents3 => agents}/hooks_serverless.py (99%) rename examples/{agents3 => agents}/nested_agents.py (97%) rename examples/{agents3 => agents}/simple.py (94%) delete mode 100644 examples/agents3-temporal/pyproject.toml delete mode 100644 examples/temporal-durable/.gitignore delete mode 100644 examples/temporal-durable/.python-version delete mode 100644 examples/temporal-durable/README.md delete mode 100644 examples/temporal-durable/activities.py delete mode 100644 examples/temporal-durable/main.py delete mode 100644 examples/temporal-durable/uv.lock delete mode 100644 examples/temporal-durable/workflow.py delete mode 100644 src/vercel_ai_sdk/agents/context.py rename src/vercel_ai_sdk/{agents3 => agents}/durability.py (100%) delete mode 100644 src/vercel_ai_sdk/agents/streams.py delete mode 100644 src/vercel_ai_sdk/agents/tools.py delete mode 100644 src/vercel_ai_sdk/agents3/__init__.py delete mode 100644 src/vercel_ai_sdk/agents3/agent.py delete mode 100644 src/vercel_ai_sdk/agents3/checkpoint.py delete mode 100644 src/vercel_ai_sdk/agents3/hooks.py delete mode 100644 src/vercel_ai_sdk/agents3/runtime.py delete mode 100644 tests/agents/test_streams.py diff --git a/examples/agents3-temporal/activities.py b/examples/agents-temporal/activities.py similarity index 100% rename from examples/agents3-temporal/activities.py rename to examples/agents-temporal/activities.py diff --git a/examples/agents3-temporal/direct.py b/examples/agents-temporal/direct.py similarity index 98% rename from examples/agents3-temporal/direct.py rename to examples/agents-temporal/direct.py index 6364977e..24e0b52a 100644 --- a/examples/agents3-temporal/direct.py +++ b/examples/agents-temporal/direct.py @@ -23,7 +23,7 @@ import activities import vercel_ai_sdk as ai - from vercel_ai_sdk.agents3 import Context, agent, tool + from vercel_ai_sdk.agents import Context, agent, tool # ── Tools ──────────────────────────────────────────────────────── diff --git a/examples/agents3-temporal/main.py b/examples/agents-temporal/main.py similarity index 95% rename from examples/agents3-temporal/main.py rename to examples/agents-temporal/main.py index 042e16f1..4b269e4d 100644 --- a/examples/agents3-temporal/main.py +++ b/examples/agents-temporal/main.py @@ -27,7 +27,7 @@ import temporalio.client import temporalio.worker -TASK_QUEUE = "agents3-durable" +TASK_QUEUE = "agents-durable" async def main(mode: str, user_query: str) -> None: @@ -50,7 +50,7 @@ async def main(mode: str, user_query: str) -> None: activities.tool_dispatch_activity, ], ): - workflow_id = f"agents3-{mode}-{uuid.uuid4().hex[:8]}" + workflow_id = f"agents-{mode}-{uuid.uuid4().hex[:8]}" print(f"Mode: {mode}") print(f"Workflow: {workflow_id}") print(f"Query: {user_query}\n") diff --git a/examples/agents3-temporal/provider.py b/examples/agents-temporal/provider.py similarity index 99% rename from examples/agents3-temporal/provider.py rename to examples/agents-temporal/provider.py index dee59bcd..bd5891ac 100644 --- a/examples/agents3-temporal/provider.py +++ b/examples/agents-temporal/provider.py @@ -24,7 +24,7 @@ import activities import vercel_ai_sdk as ai - from vercel_ai_sdk.agents3 import ( + from vercel_ai_sdk.agents import ( Checkpoint, agent, tool, diff --git a/examples/temporal-durable/pyproject.toml b/examples/agents-temporal/pyproject.toml similarity index 90% rename from examples/temporal-durable/pyproject.toml rename to examples/agents-temporal/pyproject.toml index dcd7b894..787e4328 100644 --- a/examples/temporal-durable/pyproject.toml +++ b/examples/agents-temporal/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "temporal-durable" +name = "agents-temporal" version = "0.1.0" description = "Durable agent execution with Temporal" requires-python = ">=3.12" diff --git a/examples/agents3-temporal/uv.lock b/examples/agents-temporal/uv.lock similarity index 100% rename from examples/agents3-temporal/uv.lock rename to examples/agents-temporal/uv.lock diff --git a/examples/agents3/custom_loop.py b/examples/agents/custom_loop.py similarity index 97% rename from examples/agents3/custom_loop.py rename to examples/agents/custom_loop.py index 8035be7f..3d4b7885 100644 --- a/examples/agents3/custom_loop.py +++ b/examples/agents/custom_loop.py @@ -4,7 +4,7 @@ from collections.abc import AsyncGenerator import vercel_ai_sdk as ai -from vercel_ai_sdk.agents3 import Context, agent, tool +from vercel_ai_sdk.agents import Context, agent, tool @tool diff --git a/examples/agents3/hooks.py b/examples/agents/hooks.py similarity index 97% rename from examples/agents3/hooks.py rename to examples/agents/hooks.py index 75215ef4..9ccaf42f 100644 --- a/examples/agents3/hooks.py +++ b/examples/agents/hooks.py @@ -12,7 +12,7 @@ import pydantic import vercel_ai_sdk as ai -from vercel_ai_sdk.agents3 import Context, agent, hook, resolve_hook, tool +from vercel_ai_sdk.agents import Context, agent, hook, resolve_hook, tool class Approval(pydantic.BaseModel): diff --git a/examples/agents3/hooks_serverless.py b/examples/agents/hooks_serverless.py similarity index 99% rename from examples/agents3/hooks_serverless.py rename to examples/agents/hooks_serverless.py index eab88d17..0280332a 100644 --- a/examples/agents3/hooks_serverless.py +++ b/examples/agents/hooks_serverless.py @@ -17,7 +17,7 @@ import pydantic import vercel_ai_sdk as ai -from vercel_ai_sdk.agents3 import ( +from vercel_ai_sdk.agents import ( Context, EventLogProvider, agent, diff --git a/examples/agents3/nested_agents.py b/examples/agents/nested_agents.py similarity index 97% rename from examples/agents3/nested_agents.py rename to examples/agents/nested_agents.py index 32a3d2a5..b3e5a695 100644 --- a/examples/agents3/nested_agents.py +++ b/examples/agents/nested_agents.py @@ -4,7 +4,7 @@ from collections.abc import AsyncGenerator import vercel_ai_sdk as ai -from vercel_ai_sdk.agents3 import agent, tool +from vercel_ai_sdk.agents import agent, tool model = ai.Model( id="anthropic/claude-sonnet-4-20250514", diff --git a/examples/agents3/simple.py b/examples/agents/simple.py similarity index 94% rename from examples/agents3/simple.py rename to examples/agents/simple.py index dc2fc5ad..8c5aaf15 100644 --- a/examples/agents3/simple.py +++ b/examples/agents/simple.py @@ -3,7 +3,7 @@ import asyncio import vercel_ai_sdk as ai -from vercel_ai_sdk.agents3 import agent, tool +from vercel_ai_sdk.agents import agent, tool @tool diff --git a/examples/agents3-temporal/pyproject.toml b/examples/agents3-temporal/pyproject.toml deleted file mode 100644 index 8a5dc5eb..00000000 --- a/examples/agents3-temporal/pyproject.toml +++ /dev/null @@ -1,12 +0,0 @@ -[project] -name = "agents3-temporal" -version = "0.1.0" -description = "Durable agent execution with Temporal (agents3 API)" -requires-python = ">=3.12" -dependencies = [ - "vercel-ai-sdk", - "temporalio>=1.9.0", -] - -[tool.uv.sources] -vercel-ai-sdk = { path = "../..", editable = true } diff --git a/examples/temporal-durable/.gitignore b/examples/temporal-durable/.gitignore deleted file mode 100644 index 393fc622..00000000 --- a/examples/temporal-durable/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Python -__pycache__/ -*.py[cod] -*.egg-info/ -.venv/ -dist/ - -# Environment -.env -.env*.local diff --git a/examples/temporal-durable/.python-version b/examples/temporal-durable/.python-version deleted file mode 100644 index e4fba218..00000000 --- a/examples/temporal-durable/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.12 diff --git a/examples/temporal-durable/README.md b/examples/temporal-durable/README.md deleted file mode 100644 index 2e6e8400..00000000 --- a/examples/temporal-durable/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Durable Agent Execution with Temporal - -An agent (weather + population tools) running as a Temporal workflow. -Every LLM call and tool call is a durable activity — the agent survives -crashes and restarts because Temporal replays activity results from its -event history. - -## How it works - -The framework and Temporal compose via plain async/await: - -- **Tools**: `@ai.tool` with `execute_activity()` in the body — each tool - is its own activity with a matching signature -- **LLM**: `DurableModel` wraps an activity call into a `LanguageModel`; - the activity uses `llm.buffer()` and `ToolSchema` -- **Loop**: `ai.stream_loop()` runs the agent loop unchanged -- **Bus**: `ai.run()` provides the unified message bus for streaming - -The agent function is identical to the non-Temporal version. -Temporal doesn't know about the framework; the framework doesn't know -about Temporal. - -**3 files:** `activities.py` (I/O), `workflow.py` (agent + wrappers), `main.py` - -``` -Workflow (deterministic) Activities (real I/O) -┌─────────────────────────┐ ┌──────────────────────┐ -│ while True: │ │ │ -│ response = activity───┼─────────>│ llm_call(messages) │ -│ │<─────────┼ → Anthropic API │ -│ if no tool_calls: │ │ │ -│ return text │ │ │ -│ │ │ │ -│ gather( │ │ │ -│ activity(tool1) ────┼─────────>│ get_weather(city) │ -│ activity(tool2) ────┼─────────>│ get_population(city)│ -│ ) │<─────────┼ → plain functions │ -└─────────────────────────┘ └──────────────────────┘ -``` - -On crash/restart, Temporal replays activity results from its event history. -The workflow re-executes deterministically — each `execute_activity()` call -returns the cached result instead of re-running the I/O. - -## Setup - -```bash -# 1. Install & start Temporal -brew install temporal -temporal server start-dev - -# 2. Install deps -cd examples/temporal-durable -uv sync - -# 3. Set API key -export AI_GATEWAY_API_KEY=... - -# 4. Run -uv run python main.py -``` diff --git a/examples/temporal-durable/activities.py b/examples/temporal-durable/activities.py deleted file mode 100644 index 22cc864d..00000000 --- a/examples/temporal-durable/activities.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Temporal activities — all real I/O lives here. - -Each tool is its own activity with a plain function signature. -The LLM activity uses ToolSchema (no dummy fn) and llm.buffer() -(no manual drain loop). -""" - -from __future__ import annotations - -import dataclasses -from typing import Any - -import temporalio.activity - -import vercel_ai_sdk as ai - -# ── Tool activities (one per tool, plain functions) ─────────────── - - -@temporalio.activity.defn(name="get_weather") -async def get_weather_activity(city: str) -> str: - return f"Sunny, 72F in {city}" - - -@temporalio.activity.defn(name="get_population") -async def get_population_activity(city: str) -> int: - return {"new york": 8_336_817, "los angeles": 3_979_576}.get( - city.lower(), 1_000_000 - ) - - -# ── LLM activity ───────────────────────────────────────────────── - - -@dataclasses.dataclass -class LLMCallParams: - messages: list[dict[str, Any]] - tool_schemas: list[dict[str, Any]] - - -@dataclasses.dataclass -class LLMCallResult: - message: dict[str, Any] # serialized ai.Message - - -@temporalio.activity.defn(name="llm_call") -async def llm_call_activity(params: LLMCallParams) -> LLMCallResult: - """Call the LLM, drain the stream, return the final message.""" - llm = ai.ai_gateway.GatewayModel(model="anthropic/claude-opus-4.6") - - messages = [ai.Message.model_validate(m) for m in params.messages] - tools = [ai.ToolSchema.model_validate(t) for t in params.tool_schemas] - - result = await llm.buffer(messages, tools) - return LLMCallResult(message=result.model_dump()) diff --git a/examples/temporal-durable/main.py b/examples/temporal-durable/main.py deleted file mode 100644 index c8af4266..00000000 --- a/examples/temporal-durable/main.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Entry point — starts a Temporal worker and executes the agent workflow. - -Prerequisites: - 1. Temporal dev server: temporal server start-dev - 2. AI_GATEWAY_API_KEY environment variable set - -Usage: - uv run python main.py - uv run python main.py "What is the weather in Tokyo?" -""" - -from __future__ import annotations - -import asyncio -import sys -import uuid - -import activities -import temporalio.client -import temporalio.worker -import workflow - -TASK_QUEUE = "agent-durable" - - -async def main(user_query: str) -> None: - temporal = await temporalio.client.Client.connect("localhost:7233") - - async with temporalio.worker.Worker( - temporal, - task_queue=TASK_QUEUE, - workflows=[workflow.AgentWorkflow], - activities=[ - activities.llm_call_activity, - activities.get_weather_activity, - activities.get_population_activity, - ], - ): - workflow_id = f"agent-durable-{uuid.uuid4().hex[:8]}" - print(f"Workflow {workflow_id}") - print(f"Query: {user_query}\n") - - result = await temporal.execute_workflow( - workflow.AgentWorkflow.run, - user_query, - id=workflow_id, - task_queue=TASK_QUEUE, - ) - print(result) - - -if __name__ == "__main__": - query = ( - sys.argv[1] - if len(sys.argv) > 1 - else ("What's the weather and population of New York and Los Angeles?") - ) - asyncio.run(main(query)) diff --git a/examples/temporal-durable/uv.lock b/examples/temporal-durable/uv.lock deleted file mode 100644 index f4dc0c58..00000000 --- a/examples/temporal-durable/uv.lock +++ /dev/null @@ -1,900 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.12" - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "anthropic" -version = "0.79.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "docstring-parser" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/15/b1/91aea3f8fd180d01d133d931a167a78a3737b3fd39ccef2ae8d6619c24fd/anthropic-0.79.0.tar.gz", hash = "sha256:8707aafb3b1176ed6c13e2b1c9fb3efddce90d17aee5d8b83a86c70dcdcca871", size = 509825, upload-time = "2026-02-07T18:06:18.388Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/b2/cc0b8e874a18d7da50b0fda8c99e4ac123f23bf47b471827c5f6f3e4a767/anthropic-0.79.0-py3-none-any.whl", hash = "sha256:04cbd473b6bbda4ca2e41dd670fe2f829a911530f01697d0a1e37321eb75f3cf", size = 405918, upload-time = "2026-02-07T18:06:20.246Z" }, -] - -[[package]] -name = "anyio" -version = "4.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, -] - -[[package]] -name = "attrs" -version = "25.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, -] - -[[package]] -name = "certifi" -version = "2026.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "cryptography" -version = "46.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, -] - -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, -] - -[[package]] -name = "docstring-parser" -version = "0.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "jiter" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, - { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, - { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, - { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, - { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, - { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, - { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, - { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, - { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, - { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, - { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, - { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, - { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, - { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, - { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, - { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, - { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, - { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, - { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, - { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, - { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, - { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, - { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, - { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, - { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, - { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, - { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, - { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, - { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, -] - -[[package]] -name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - -[[package]] -name = "mcp" -version = "1.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, -] - -[[package]] -name = "nexus-rpc" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2e/f2/d54f5c03d8f4672ccc0875787a385f53dcb61f98a8ae594b5620e85b9cb3/nexus_rpc-1.3.0.tar.gz", hash = "sha256:e56d3b57b60d707ce7a72f83f23f106b86eca1043aa658e44582ab5ff30ab9ad", size = 75650, upload-time = "2025-12-08T22:59:13.002Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/74/0afd841de3199c148146c1d43b4bfb5605b2f1dc4c9a9087fe395091ea5a/nexus_rpc-1.3.0-py3-none-any.whl", hash = "sha256:aee0707b4861b22d8124ecb3f27d62dafbe8777dc50c66c91e49c006f971b92d", size = 28873, upload-time = "2025-12-08T22:59:12.024Z" }, -] - -[[package]] -name = "openai" -version = "2.20.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6e/5a/f495777c02625bfa18212b6e3b73f1893094f2bf660976eb4bc6f43a1ca2/openai-2.20.0.tar.gz", hash = "sha256:2654a689208cd0bf1098bb9462e8d722af5cbe961e6bba54e6f19fb843d88db1", size = 642355, upload-time = "2026-02-10T19:02:54.145Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/a0/cf4297aa51bbc21e83ef0ac018947fa06aea8f2364aad7c96cbf148590e6/openai-2.20.0-py3-none-any.whl", hash = "sha256:38d989c4b1075cd1f76abc68364059d822327cf1a932531d429795f4fc18be99", size = 1098479, upload-time = "2026-02-10T19:02:52.157Z" }, -] - -[[package]] -name = "protobuf" -version = "6.33.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, - { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, - { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, -] - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, -] - -[[package]] -name = "pydantic-settings" -version = "2.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, -] - -[[package]] -name = "pyjwt" -version = "2.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, -] - -[[package]] -name = "python-multipart" -version = "0.0.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, -] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, -] - -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - -[[package]] -name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - -[[package]] -name = "sse-starlette" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, -] - -[[package]] -name = "starlette" -version = "0.52.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, -] - -[[package]] -name = "temporal-durable" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "temporalio" }, - { name = "vercel-ai-sdk" }, -] - -[package.metadata] -requires-dist = [ - { name = "temporalio", specifier = ">=1.9.0" }, - { name = "vercel-ai-sdk", editable = "../../" }, -] - -[[package]] -name = "temporalio" -version = "1.22.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nexus-rpc" }, - { name = "protobuf" }, - { name = "types-protobuf" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b5/28/2a79a1e98e4280924f08ea0989ee045aa0b65f17f8d4f2ae7b53c2f4c38d/temporalio-1.22.0.tar.gz", hash = "sha256:896452fad246de2277cbb0408e4e0899882da1843480d5cbb57c7a5767440834", size = 1906162, upload-time = "2026-02-03T20:58:37.042Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/00/4dd57b3be03cc22523fd7083a3f63700188d7856ada875c99e71ca9a72bd/temporalio-1.22.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:532ef90cdee487a76c46eb71348f94f0a8a432e9dd241a0552b384314bbe28c0", size = 12198015, upload-time = "2026-02-03T20:57:04.056Z" }, - { url = "https://files.pythonhosted.org/packages/a7/15/1c24ea8005f1abc58dbb35b26ea93e5067afc385e56dda0e50c64c75cc07/temporalio-1.22.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:2a5f8646c1a0c36d5d4472462f2d315bafa7e2c9b1f52f15a07d01d1ccc33778", size = 11697647, upload-time = "2026-02-03T20:57:26.252Z" }, - { url = "https://files.pythonhosted.org/packages/09/2e/b65ec41f73030a109c253ac30545e4aac19044cd30083231b9b5993914e8/temporalio-1.22.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8cf7c909f7d6bc236a45aa09fc347cb53b02fa3287df29409d0500fc21c0dc5", size = 11972722, upload-time = "2026-02-03T20:57:47.371Z" }, - { url = "https://files.pythonhosted.org/packages/b4/de/048bf901417940f62bd69243d95762a63e97a5ad138c514c76852d364cd6/temporalio-1.22.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac4adf1ac20f594066b8091de22f463a31f0926c23be1e990519fd6dbbb9d1b", size = 12275101, upload-time = "2026-02-03T20:58:09.636Z" }, - { url = "https://files.pythonhosted.org/packages/14/8e/f5852ef5326990ae5a15cb5f764254df1d086463ddc8244766d13872c3d3/temporalio-1.22.0-cp310-abi3-win_amd64.whl", hash = "sha256:4453ba03681e4ed39bf410f76997b7e0b9ec239d0dff7cabd53eb89c7fbaa6b0", size = 12713408, upload-time = "2026-02-03T20:58:32.086Z" }, -] - -[[package]] -name = "tqdm" -version = "4.67.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, -] - -[[package]] -name = "types-protobuf" -version = "6.32.1.20251210" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/59/c743a842911887cd96d56aa8936522b0cd5f7a7f228c96e81b59fced45be/types_protobuf-6.32.1.20251210.tar.gz", hash = "sha256:c698bb3f020274b1a2798ae09dc773728ce3f75209a35187bd11916ebfde6763", size = 63900, upload-time = "2025-12-10T03:14:25.451Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/43/58e75bac4219cbafee83179505ff44cae3153ec279be0e30583a73b8f108/types_protobuf-6.32.1.20251210-py3-none-any.whl", hash = "sha256:2641f78f3696822a048cfb8d0ff42ccd85c25f12f871fbebe86da63793692140", size = 77921, upload-time = "2025-12-10T03:14:24.477Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.40.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, -] - -[[package]] -name = "vercel" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "websockets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/69/566d75db8b86cca1884fe9d0eb063587a884c899e9a3cb52ef1723d22733/vercel-0.4.0.tar.gz", hash = "sha256:a8bc19823de2b6ac12b514b32af0823ac40b608c8dbb77386bd8ce965f6f6d94", size = 56643, upload-time = "2026-02-12T19:05:46.84Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/c7/32d878612d6cc14f394f5440cb400793759eac657d52c992555fe010763a/vercel-0.4.0-py3-none-any.whl", hash = "sha256:d842b328222ed835b280adb7f61df560e68592c70aad9478ca2d9ab81188864d", size = 72659, upload-time = "2026-02-12T19:05:45.678Z" }, -] - -[[package]] -name = "vercel-ai-sdk" -version = "0.0.1.dev3" -source = { editable = "../../" } -dependencies = [ - { name = "anthropic" }, - { name = "httpx" }, - { name = "mcp" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "vercel" }, -] - -[package.metadata] -requires-dist = [ - { name = "anthropic", specifier = ">=0.40.0" }, - { name = "httpx", specifier = ">=0.28.1" }, - { name = "mcp", specifier = ">=1.18.0" }, - { name = "openai", specifier = ">=2.14.0" }, - { name = "pydantic", specifier = ">=2.12.5" }, - { name = "vercel", specifier = ">=0.3.8" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "pytest", specifier = ">=8.0" }, - { name = "pytest-asyncio", specifier = ">=0.24" }, - { name = "python-dotenv", specifier = ">=1.2.1" }, - { name = "rich", specifier = ">=14.2.0" }, -] - -[[package]] -name = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, -] diff --git a/examples/temporal-durable/workflow.py b/examples/temporal-durable/workflow.py deleted file mode 100644 index 4bbd2b10..00000000 --- a/examples/temporal-durable/workflow.py +++ /dev/null @@ -1,150 +0,0 @@ -"""Temporal workflow — the durable agent loop. - -NOTE: This example still uses the old models.LanguageModel ABC because -it wraps Temporal activities as a custom model. When the models layer -is fully migrated to models, this will need a custom adapter instead. -""" - -from __future__ import annotations - -import asyncio -import datetime -from collections.abc import AsyncGenerator, Awaitable, Callable, Sequence -from typing import Any, override - -import pydantic -import temporalio.common -import temporalio.workflow - -with temporalio.workflow.unsafe.imports_passed_through(): - import activities - - import vercel_ai_sdk as ai - - -class DurableModel(ai.models.LanguageModel): - def __init__( - self, - call_fn: Callable[ - [activities.LLMCallParams], Awaitable[activities.LLMCallResult] - ], - ) -> None: - self.call_fn = call_fn - - @override - async def stream( - self, - messages: list[ai.Message], - tools: Sequence[ai.ToolLike] | None = None, - output_type: type[pydantic.BaseModel] | None = None, - ) -> AsyncGenerator[ai.Message]: - result = await self.call_fn( - activities.LLMCallParams( - messages=[m.model_dump() for m in messages], - tool_schemas=[ - { - "name": t.name, - "description": t.description, - "param_schema": t.param_schema, - } - for t in (tools or []) - ], - ) - ) - yield ai.Message.model_validate(result.message) - - -# ── Durable tools ──────────────────────────────────────────────── -# -# Plain @ai.tool — the decorator builds the JSON schema from the -# signature. The body calls execute_activity, making each tool -# invocation a durable Temporal activity. - - -@ai.tool -async def get_weather(city: str) -> str: - """Get current weather for a city.""" - return await temporalio.workflow.execute_activity( - activities.get_weather_activity, - args=[city], - start_to_close_timeout=datetime.timedelta(minutes=2), - ) - - -@ai.tool -async def get_population(city: str) -> int: - """Get population of a city.""" - return await temporalio.workflow.execute_activity( - activities.get_population_activity, - args=[city], - start_to_close_timeout=datetime.timedelta(minutes=2), - ) - - -# ── Agent ──────────────────────────────────────────────────────── -# -# TODO: This example uses the old LanguageModel ABC and ai.run() / -# ai.stream_loop free-function patterns. Once the models layer is -# migrated, convert to use ai.agent() + models.Model with a custom -# adapter for Temporal activity-based LLM calls. - - -async def agent(llm: Any, user_query: str) -> ai.StreamResult: - """Agent loop — uses old-style stream_loop via models.LanguageModel. - - This is a transitional pattern. The old ai.stream_loop and ai.run - are no longer part of the public API. This example needs a custom - models adapter to work with the new Agent API. - """ - messages = [ - ai.system_message("Answer questions using the weather and population tools."), - ai.user_message(user_query), - ] - - # Manually implement the loop since we can't use Agent with LanguageModel - tools = [get_weather, get_population] - local_messages = list(messages) - - while True: - result_messages: list[ai.Message] = [] - async for msg in llm.stream(local_messages, tools=tools): - result_messages.append(msg) - result = ai.StreamResult(messages=result_messages) - - if not result.tool_calls: - return result - - last_msg = result.last_message - if last_msg is not None: - local_messages.append(last_msg) - - await asyncio.gather( - *(ai.execute_tool(tc, message=last_msg) for tc in result.tool_calls) - ) - - -# ── Workflow ───────────────────────────────────────────────────── - - -@temporalio.workflow.defn -class AgentWorkflow: - @temporalio.workflow.run - async def run(self, user_query: str) -> str: - llm = DurableModel( - lambda params: temporalio.workflow.execute_activity( - activities.llm_call_activity, - params, - start_to_close_timeout=datetime.timedelta(minutes=5), - retry_policy=temporalio.common.RetryPolicy(maximum_attempts=3), - ) - ) - - # TODO: This uses the old free-function pattern. Once models - # supports custom adapters for Temporal, use Agent.run() instead. - from vercel_ai_sdk.agents import run - - final_text = "" - async for msg in run(agent, llm, user_query): - if msg.text: - final_text = msg.text - return final_text diff --git a/src/vercel_ai_sdk/__init__.py b/src/vercel_ai_sdk/__init__.py index fe2163c9..3b23fc0a 100644 --- a/src/vercel_ai_sdk/__init__.py +++ b/src/vercel_ai_sdk/__init__.py @@ -1,28 +1,24 @@ from . import adapters, models, telemetry from .adapters import ai_sdk_ui from .agents import ( + TOOL_APPROVAL_HOOK_TYPE, Agent, - AgentRun, Checkpoint, Context, - Hook, - HookInfo, - LoopFn, + DurabilityProvider, + EventLogProvider, + HookEvent, PendingHookInfo, - RunResult, - Runtime, - StreamResult, + StepEvent, Tool, ToolApproval, - ToolSource, + ToolCall, + ToolEvent, agent, - execute_tool, - get_checkpoint, - get_context, + cancel_hook, hook, mcp, - stream, - stream_step, + resolve_hook, tool, ) from .models import Client, Model, ModelCost @@ -87,33 +83,27 @@ "models", # Agents — primary API "Agent", - "AgentRun", "agent", - "LoopFn", - # Agents — composition primitives - "stream_step", - "execute_tool", - "get_checkpoint", - "stream", - "StreamResult", + "Context", # Agents — tools "Tool", + "ToolCall", "tool", # Agents — hooks - "Hook", "hook", + "resolve_hook", + "cancel_hook", "ToolApproval", - # Agents — context - "Context", - "ToolSource", - "get_context", - # Agents — runtime (developer API) - "Runtime", - "RunResult", - "HookInfo", + "TOOL_APPROVAL_HOOK_TYPE", + # Agents — durability + "DurabilityProvider", + "EventLogProvider", # Agents — checkpoint "Checkpoint", "PendingHookInfo", + "StepEvent", + "ToolEvent", + "HookEvent", # Submodules "telemetry", "mcp", diff --git a/src/vercel_ai_sdk/_durability.py b/src/vercel_ai_sdk/_durability.py index fa041676..4f85fe97 100644 --- a/src/vercel_ai_sdk/_durability.py +++ b/src/vercel_ai_sdk/_durability.py @@ -1,9 +1,9 @@ """Shared durability context var. Lives at the package root so that both ``models`` (lower-level) and -``agents3`` (higher-level) can import it without circular dependencies. +``agents`` (higher-level) can import it without circular dependencies. The actual ``DurabilityProvider`` protocol and implementations live in -``agents3.durability``; this module only holds the context var and a +``agents.durability``; this module only holds the context var and a thin accessor. """ @@ -13,10 +13,10 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from .agents3 import durability + from .agents import durability # The context var stores Any at runtime to avoid importing the protocol -# at module level. ``agents3.durability`` provides the typed accessor. +# at module level. ``agents.durability`` provides the typed accessor. _provider: contextvars.ContextVar[Any] = contextvars.ContextVar( "durability_provider", default=None ) diff --git a/src/vercel_ai_sdk/adapters/ai_sdk_ui/adapter.py b/src/vercel_ai_sdk/adapters/ai_sdk_ui/adapter.py index 59c78e57..01b0bb62 100644 --- a/src/vercel_ai_sdk/adapters/ai_sdk_ui/adapter.py +++ b/src/vercel_ai_sdk/adapters/ai_sdk_ui/adapter.py @@ -11,6 +11,7 @@ from typing import Any from ...agents import hooks +from ...agents.hooks import TOOL_APPROVAL_HOOK_TYPE from ...types import messages as messages_ from . import protocol, ui_message @@ -138,7 +139,7 @@ def _tool_call_id_from_approval_hook( Returns the tool_call_id if this is a ToolApproval hook whose hook_id follows the ``approve_{tool_call_id}`` convention, otherwise None. """ - if hook_part.hook_type != hooks.ToolApproval.hook_type: # type: ignore[attr-defined] + if hook_part.hook_type != TOOL_APPROVAL_HOOK_TYPE: return None prefix = "approve_" if hook_part.hook_id.startswith(prefix): @@ -463,7 +464,7 @@ def to_messages( and tp.approval is not None and tp.approval.approved is not None ): - hooks.ToolApproval.resolve( # type: ignore[attr-defined] + hooks.resolve_hook( tp.approval.id, { "granted": tp.approval.approved, diff --git a/src/vercel_ai_sdk/agents/__init__.py b/src/vercel_ai_sdk/agents/__init__.py index d7a62b0f..48e184e6 100644 --- a/src/vercel_ai_sdk/agents/__init__.py +++ b/src/vercel_ai_sdk/agents/__init__.py @@ -1,64 +1,33 @@ -"""Agent loop orchestration — tools, hooks, runtime, and streaming. - -Depends on types/ and models/. Provides the loop machinery that -plugs a model into a tool-calling loop with hooks and checkpoints. -""" - from . import mcp -from .agent import Agent, AgentRun, LoopFn, agent, stream_step -from .checkpoint import Checkpoint, PendingHookInfo -from .context import Context, ToolSource, get_context -from .hooks import Hook, ToolApproval, hook -from .runtime import ( - EventLog, - HookInfo, - LoopExecutor, - RunResult, - Runtime, - execute_tool, - get_checkpoint, - run, +from .agent import Agent, Context, Tool, ToolCall, agent, tool +from .checkpoint import Checkpoint, HookEvent, PendingHookInfo, StepEvent, ToolEvent +from .durability import DurabilityProvider, EventLogProvider +from .hooks import ( + TOOL_APPROVAL_HOOK_TYPE, + ToolApproval, + cancel_hook, + hook, + resolve_hook, ) -from .streams import StreamResult, stream -from .tools import Tool, ToolLike, ToolSchema, get_tool, tool __all__ = [ - # Agent (primary user API) "Agent", - "AgentRun", - "agent", - "LoopFn", - # Composition primitives - "stream_step", - "execute_tool", - "get_checkpoint", - # Context + "Checkpoint", "Context", - "ToolSource", - "get_context", - # Runtime (developer API) - "Runtime", - "EventLog", - "LoopExecutor", - "RunResult", - "HookInfo", - "run", - # Stream - "stream", - "StreamResult", - # Tools + "DurabilityProvider", + "EventLogProvider", + "HookEvent", + "PendingHookInfo", + "StepEvent", "Tool", - "ToolLike", - "ToolSchema", - "tool", - "get_tool", - # Hooks - "Hook", + "ToolCall", + "ToolEvent", + "agent", + "cancel_hook", "hook", - "ToolApproval", - # Checkpoint - "Checkpoint", - "PendingHookInfo", - # Submodules "mcp", + "resolve_hook", + "tool", + "ToolApproval", + "TOOL_APPROVAL_HOOK_TYPE", ] diff --git a/src/vercel_ai_sdk/agents/agent.py b/src/vercel_ai_sdk/agents/agent.py index acaf51e7..e54c68dc 100644 --- a/src/vercel_ai_sdk/agents/agent.py +++ b/src/vercel_ai_sdk/agents/agent.py @@ -1,268 +1,311 @@ -"""Agent — the primary user-facing API. - -Bundles model, system prompt, and tools into a reusable, composable -unit. Provides a default tool-calling loop and a decorator to -override it. - -Usage:: - - agent = ai.agent( - model=my_model, - system="You are a helpful assistant.", - tools=[get_weather, get_population], - ) - - # stream messages - async for msg in agent.run(messages): - print(msg.text_delta, end="") - - # or collect the final result - result = await agent.run(messages).collect() - print(result.text) -""" +"""Agent, Context, StreamResult, and the stream() function.""" from __future__ import annotations import asyncio -from collections.abc import AsyncGenerator, Awaitable, Callable, Sequence -from typing import Any +import inspect +import json +from collections.abc import AsyncGenerator, Awaitable, Callable +from typing import Any, Protocol, get_type_hints import pydantic -from .. import models -from ..types import messages as messages_ +from .. import _durability as _dctx +from .. import models, types +from ..telemetry import events as telemetry +from ..types import builders from . import checkpoint as checkpoint_ -from . import context, runtime, streams -from . import tools as tools_ - -# ── Types ───────────────────────────────────────────────────────── - -LoopFn = Callable[ - ["Agent", list[messages_.Message]], Awaitable[streams.StreamResult | None] -] - +from . import durability as durability_ +from . import runtime -# ── Composition primitives ──────────────────────────────────────── +class Tool[**P, R]: + """Wraps async function, introspects schema, attaches a validator""" -@streams.stream -async def stream_step( - model: models.Model, - messages: list[messages_.Message], - tools: Sequence[tools_.ToolLike] | None = None, - label: str | None = None, - output_type: type[pydantic.BaseModel] | None = None, - **kwargs: Any, -) -> AsyncGenerator[messages_.Message]: - """Single LLM call that streams into the Runtime queue. - - This is a composition primitive for custom ``@agent.loop`` - functions and multi-agent orchestration. It is decorated with - ``@stream``, so each call becomes a replayable step in the - event log. - """ - async for msg in await models.stream( - model, messages, tools=tools, output_type=output_type, **kwargs - ): - yield msg.model_copy(update={"label": label}) if label is not None else msg - + def __init__( + self, + fn: Callable[P, Awaitable[R]], + schema: types.ToolSchema, + validator: type[pydantic.BaseModel] | None = None, + *, + is_gen: bool = False, + ) -> None: + self._fn = fn + self._validator = validator + self._is_gen = is_gen + self.schema = schema + + async def __call__(self, json_args: str) -> R: + """Parse json_args into kwargs, validate, and call the function.""" + kwargs = json.loads(json_args) if json_args else {} + if self._validator is not None: + self._validator.model_validate(kwargs) + + if not self._is_gen: + return await self._fn(**kwargs) # type: ignore[call-arg] + + # Generator tool (e.g. agent-as-a-tool): drain the async + # generator, pipe each yielded message to the runtime for + # real-time streaming, and return the final text as the result. + rt = runtime.get_runtime() + last: types.Message | None = None + async for message in self._fn(**kwargs): # type: ignore[call-arg,attr-defined] + await rt.put_message(message) + last = message + return last.text if last else "" # type: ignore[return-value] -# ── AgentRun ────────────────────────────────────────────────────── + @property + def name(self) -> str: + return self.schema.name + @property + def description(self) -> str: + return self.schema.description -class AgentRun: - """Returned by ``agent.run()``. Async-iterate for messages, then - inspect post-run state. + @property + def param_schema(self) -> dict[str, Any]: + return self.schema.param_schema + + +def tool[**P, R](fn: Callable[P, Awaitable[R]]) -> Tool[P, R]: + """Decorator: turn an async function into a :class:`Tool`.""" + sig = inspect.signature(fn) + hints = get_type_hints(fn) if hasattr(fn, "__annotations__") else {} + + fields: dict[str, Any] = {} + for param_name, param in sig.parameters.items(): + param_type = hints.get(param_name, str) + if param.default is inspect.Parameter.empty: + fields[param_name] = (param_type, ...) + else: + fields[param_name] = (param_type, param.default) + + validator = pydantic.create_model(f"{fn.__name__}_Args", **fields) + + schema = types.ToolSchema( + name=fn.__name__, + description=inspect.getdoc(fn) or "", + param_schema=validator.model_json_schema(), + return_type=hints.get("return", None), + ) - Usage:: + return Tool( + fn=fn, + schema=schema, + validator=validator, + is_gen=inspect.isasyncgenfunction(fn), + ) - run = agent.run(messages) - # streaming - async for msg in run: - print(msg.text_delta, end="") - run.checkpoint # checkpoint after iteration - run.pending_hooks # unresolved hooks (empty if completed) +class ToolCall: + """Callable that binds a :class:`ToolCallPart` to its :class:`Tool`. - # non-streaming - result = await agent.run(messages).collect() - print(result.text) + Calling it executes the tool and returns a :class:`ToolResultPart`. """ - def __init__(self, inner: runtime.RunResult) -> None: - self._inner = inner + def __init__(self, part: types.ToolCallPart, tool: Tool[..., Any]) -> None: + self._part = part + self._tool = tool - async def __aiter__(self) -> AsyncGenerator[messages_.Message]: - async for msg in self._inner: - yield msg - - async def collect(self) -> streams.StreamResult: - """Drain the stream and return a :class:`StreamResult`.""" - msgs: list[messages_.Message] = [] - async for msg in self._inner: - msgs.append(msg) - return streams.StreamResult(messages=msgs) + @property + def id(self) -> str: + return self._part.tool_call_id @property - def checkpoint(self) -> checkpoint_.Checkpoint: - return self._inner.checkpoint + def name(self) -> str: + return self._part.tool_name @property - def pending_hooks(self) -> dict[str, runtime.HookInfo]: - return self._inner.pending_hooks + def args(self) -> str: + return self._part.tool_args + async def __call__(self) -> types.ToolResultPart: + """Execute the tool and return a :class:`ToolResultPart`. -# ── Agent ───────────────────────────────────────────────────────── + If a durability provider is active, the call is routed through + it for recording or replay. + """ + provider = _dctx.get_provider() + telemetry.handle( + telemetry.ToolCallStartEvent( + tool_name=self.name, + tool_call_id=self.id, + args=self.args, + ) + ) + t0 = telemetry.time_ms() + error_str: str | None = None + + async def _execute() -> types.ToolResultPart: + nonlocal error_str + try: + result = await self._tool(self._part.tool_args) + except Exception as exc: + error_str = str(exc) + return types.ToolResultPart( + tool_call_id=self._part.tool_call_id, + tool_name=self._part.tool_name, + result=str(exc), + is_error=True, + ) + return types.ToolResultPart( + tool_call_id=self._part.tool_call_id, + tool_name=self._part.tool_name, + result=result, + ) -class Agent: - """An agent — bundles model, system prompt, tools, and loop logic. + if provider is not None: + result_part = await provider.execute_tool( + _execute, + tool_call_id=self.id, + tool_name=self.name, + ) + else: + result_part = await _execute() + + telemetry.handle( + telemetry.ToolCallFinishEvent( + tool_name=self.name, + tool_call_id=self.id, + result=result_part.result, + error=error_str, + duration_ms=telemetry.time_ms() - t0, + ) + ) + return result_part - Create via :func:`agent`:: - weather = ai.agent( - model=my_model, - system="Answer questions about weather.", - tools=[get_weather], - ) +class Context(pydantic.BaseModel): + """Everything that goes into the LLM.""" - Tools default to all globally registered tools when ``None`` - (the default). Pass ``tools=[]`` to explicitly disable tools. + model: models.Model + messages: list[types.Message] + tools: list[Tool[..., Any]] - Override the default tool-calling loop with ``@agent.loop``:: + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) - @weather.loop - async def custom(agent, messages): - ... - """ + def resolve(self, tool_parts: list[types.ToolCallPart]) -> list[ToolCall]: + """Resolve ToolCallParts into callable ToolCall objects.""" + tools_by_name = {t.name: t for t in self.tools} + return [ + ToolCall(part=tp, tool=tools_by_name[tp.tool_name]) for tp in tool_parts + ] - def __init__( - self, - model: models.Model, - system: str = "", - tools: list[tools_.Tool[..., Any]] | None = None, - ) -> None: - self._model = model - self._system = system - self._tools = tools - self._custom_loop: LoopFn | None = None - @property - def model(self) -> models.Model: - return self._model +class LoopFn(Protocol): + def __call__(self, context: Context) -> AsyncGenerator[types.Message]: ... - @property - def system(self) -> str: - return self._system - @property - def tools(self) -> list[tools_.Tool[..., Any]]: - """Registered tools. ``None`` at init resolves to all globally - registered tools at access time.""" - if self._tools is None: - return list(tools_._tool_registry.values()) - return list(self._tools) +async def _default_loop(context: Context) -> AsyncGenerator[types.Message]: + step_index = 0 + while True: + telemetry.handle(telemetry.StepStartEvent(step_index=step_index)) - def loop(self, fn: LoopFn) -> LoopFn: - """Decorator to override the default agent loop. + stream = await models.stream( + context.model, context.messages, tools=context.tools + ) + done_message: types.Message | None = None + async for message in stream: + done_message = message + yield message + + if done_message is not None: + telemetry.handle( + telemetry.StepFinishEvent(step_index=step_index, message=done_message) + ) + step_index += 1 - The decorated function receives the :class:`Agent` instance and - the per-run messages:: + tool_calls = context.resolve(stream.tool_calls) + if not tool_calls: + break - @my_agent.loop - async def custom( - agent: ai.Agent, messages: list[ai.Message], - ) -> ai.StreamResult: - ... - """ - self._custom_loop = fn - return fn + # Execute tool calls in parallel. + async with asyncio.TaskGroup() as tg: + tasks = [tg.create_task(tc()) for tc in tool_calls] - async def _default_loop( - self, messages: list[messages_.Message] - ) -> streams.StreamResult: - """Built-in loop: stream LLM, execute tools, repeat.""" - local_messages = list(messages) + # Yield a tool-result message — history auto-collects it. + yield builders.tool_message(*(t.result() for t in tasks)) - while True: - result = await stream_step(self.model, local_messages, self.tools) - if not result.tool_calls: - return result +async def _collect_messages( + source: AsyncGenerator[types.Message], + messages: list[types.Message], +) -> AsyncGenerator[types.Message]: + """Intercept yielded messages and collect done ones into *messages*. - last_msg = result.last_message - if last_msg is None: - return result + This runs on the **producer** side (same coroutine as the loop function), + so ``messages`` is always up-to-date by the time the loop reads it for + the next model call — avoiding the race that would occur if collection + happened on the consumer side of the runtime queue. + """ + async for message in source: + if message.is_done: + for i, existing in enumerate(messages): + if existing.id == message.id: + messages[i] = message + break + else: + messages.append(message) + yield message - result_parts = await asyncio.gather( - *( - runtime.execute_tool(tc, message=last_msg) - for tc in result.tool_calls - ) - ) - # Append the assistant message (with tool calls) and a - # separate tool-result message to the local history. - local_messages.append(last_msg) - local_messages.append( - messages_.Message(role="tool", parts=list(result_parts)) - ) - def run( +class Agent: + """Bag of configuration: model + tools + loop.""" + + def __init__( self, - messages: list[messages_.Message], *, - checkpoint: checkpoint_.Checkpoint | None = None, - ) -> AgentRun: - """Run the agent. + tools: list[Tool[..., Any]] | None = None, + ) -> None: + self._tools: list[Tool[..., Any]] = tools or [] + self._loop_fn: LoopFn = _default_loop + + def loop(self, fn: LoopFn) -> LoopFn: + """Decorator: override the default loop function.""" + self._loop_fn = fn + return fn - Returns an :class:`AgentRun` — async-iterate for streamed - messages, or call ``.collect()`` for the final result. + async def run( + self, + model: models.Model, + messages: list[types.Message], + *, + durability: durability_.DurabilityProvider | None = None, + checkpoint: checkpoint_.Checkpoint | None = None, + ) -> AsyncGenerator[types.Message]: + """Run the agent loop, yielding messages to the consumer. Args: - messages: Conversation messages (user, assistant, etc.). - checkpoint: Resume from a previous checkpoint. + model: The model to use for LLM calls. + messages: Initial conversation messages. + durability: Explicit durability provider. If ``None`` but + *checkpoint* is given, an :class:`EventLogProvider` is + created automatically. + checkpoint: Checkpoint to resume from. Implies eventlog + durability when no explicit *durability* is provided. """ - # Prepend system prompt - full_messages: list[messages_.Message] = [] - if self._system: - full_messages.append( - messages_.Message( - role="system", - parts=[messages_.TextPart(text=self._system)], - ) - ) - full_messages.extend(messages) - - ctx = context.Context(tools=self.tools) - - # Build the graph function that runtime.run() expects - async def _graph() -> streams.StreamResult | None: - if self._custom_loop: - return await self._custom_loop(self, full_messages) - return await self._default_loop(full_messages) - - inner = runtime.run( - _graph, - checkpoint=checkpoint, - context=ctx, - ) - return AgentRun(inner) + # Convenience: checkpoint implies eventlog provider. + if checkpoint is not None and durability is None: + durability = durability_.EventLogProvider(checkpoint) + context = Context(model=model, messages=list(messages), tools=self._tools) -# ── Factory ─────────────────────────────────────────────────────── + # Set the durability provider on the shared context var so that + # models.stream() and ToolCall.__call__() auto-detect it. + token = _dctx.set_provider(durability) if durability is not None else None + try: + source = _collect_messages(self._loop_fn(context), context.messages) + async for message in runtime.run(source): + yield message + finally: + if token is not None: + _dctx.reset_provider(token) def agent( - model: models.Model, - system: str = "", - tools: list[tools_.Tool[..., Any]] | None = None, + *, + tools: list[Tool[..., Any]] | None = None, ) -> Agent: - """Create an :class:`Agent`. - - Args: - model: The language model to use. - system: System prompt. - tools: Tools available to the agent. ``None`` (default) means - all globally registered tools. Pass ``[]`` to disable. - """ - return Agent(model=model, system=system, tools=tools) + """Create an Agent.""" + return Agent(tools=tools) diff --git a/src/vercel_ai_sdk/agents/checkpoint.py b/src/vercel_ai_sdk/agents/checkpoint.py index 30c9bda6..6cd0d96e 100644 --- a/src/vercel_ai_sdk/agents/checkpoint.py +++ b/src/vercel_ai_sdk/agents/checkpoint.py @@ -1,3 +1,10 @@ +"""Checkpoint data model for durable agent execution. + +A Checkpoint is a serializable snapshot of all completed work in an agent +run. On re-entry, the durability provider replays cached results from the +checkpoint instead of re-executing LLM calls and tool invocations. +""" + from __future__ import annotations from typing import Any @@ -5,23 +12,20 @@ import pydantic from ..types import messages as messages_ -from . import streams class StepEvent(pydantic.BaseModel): - """A completed @stream step.""" + """A completed LLM stream step — stores the final done message.""" index: int - messages: list[messages_.Message] - - def to_stream_result(self) -> streams.StreamResult: - return streams.StreamResult(messages=list(self.messages)) + message: messages_.Message class ToolEvent(pydantic.BaseModel): """A completed tool execution.""" tool_call_id: str + tool_name: str result: Any status: str = "result" # "result" | "error" @@ -37,11 +41,13 @@ class PendingHookInfo(pydantic.BaseModel): """A hook that was suspended but not resolved when the run ended.""" label: str - hook_type: str + payload_type: str # fully qualified name of the pydantic model metadata: dict[str, Any] = {} class Checkpoint(pydantic.BaseModel): + """Serializable snapshot of all completed work in an agent run.""" + steps: list[StepEvent] = [] tools: list[ToolEvent] = [] hooks: list[HookEvent] = [] diff --git a/src/vercel_ai_sdk/agents/context.py b/src/vercel_ai_sdk/agents/context.py deleted file mode 100644 index 73dd56b1..00000000 --- a/src/vercel_ai_sdk/agents/context.py +++ /dev/null @@ -1,206 +0,0 @@ -"""Context — everything the LLM sees during a run. - -Consolidates tool registry, system prompt, message history, and model -reference into a single, serializable object. Independent of execution -machinery (Runtime) — can be constructed, inspected, and serialized -without starting a run. - -The context is stashed in a contextvar during ``run()`` so that -framework internals (``execute_tool``, MCP client, etc.) can access it. -""" - -from __future__ import annotations - -import contextvars -from collections.abc import Sequence -from typing import TYPE_CHECKING, Any - -import pydantic - -from ..types import messages as messages_ - -if TYPE_CHECKING: - from . import tools as tools_ - - -# ── ToolSource ──────────────────────────────────────────────────── - - -class ToolSource(pydantic.BaseModel): - """Provenance info for a tool — how to find or reconstruct it. - - Carries enough information to locate the code behind a tool, - whether it's a decorated Python function or an MCP server. - - Attributes: - kind: ``"python"``, ``"mcp_stdio"``, or ``"mcp_http"``. - module: Python module path, e.g. ``"myapp.tools"``. - qualname: Qualified name, e.g. ``"get_weather"``. - uri: Remote URL for HTTP-based MCP servers. - server_command: Launch command for stdio MCP servers. - """ - - model_config = pydantic.ConfigDict(frozen=True) - - kind: str - module: str | None = None - qualname: str | None = None - uri: str | None = None - server_command: str | None = None - - -# ── Context ─────────────────────────────────────────────────────── - - -class Context(pydantic.BaseModel): - """Everything the LLM sees: tools, system prompt, messages, model. - - Independent of execution machinery (Runtime). Constructable by the - user or auto-constructed by ``run()``. - - Usage:: - - ctx = Context( - system_prompt="You are a helpful assistant.", - tools=[get_weather, get_population], - ) - ctx.get_tool("get_weather") # look up by name - data = ctx.model_dump() # serializable snapshot - """ - - model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) - - model: Any = None - system_prompt: str = "" - messages: list[messages_.Message] = pydantic.Field(default_factory=list) - - _tools: dict[str, tools_.Tool[..., Any]] = pydantic.PrivateAttr( - default_factory=dict - ) - - def __init__( - self, - *, - tools: Sequence[tools_.Tool[..., Any]] | None = None, - **data: Any, - ) -> None: - super().__init__(**data) - if tools: - for t in tools: - self.register_tool(t) - - # ── Tool registry (scoped to this context) ──────────────── - - def register_tool(self, tool: tools_.Tool[..., Any]) -> None: - """Register a tool in this context's scoped registry.""" - self._tools[tool.name] = tool - - def get_tool(self, name: str) -> tools_.Tool[..., Any] | None: - """Look up a tool by name. Returns ``None`` if not found.""" - return self._tools.get(name) - - @property - def tools(self) -> list[tools_.Tool[..., Any]]: - """All tools registered in this context.""" - return list(self._tools.values()) - - @property - def tool_schemas(self) -> list[tools_.ToolSchema]: - """Tool schemas — what gets sent to the LLM.""" - return [t.schema for t in self._tools.values()] - - # ── Serialization ───────────────────────────────────────── - - @pydantic.model_serializer - def _serialize(self) -> dict[str, Any]: - """Serialize including tool schemas and sources. - - Tool code is not serialized — only schemas and source - references. - """ - return { - "system_prompt": self.system_prompt, - "messages": [m.model_dump() for m in self.messages], - "tools": [ - { - "schema": t.schema.model_dump(), - "source": (t.source.model_dump() if t.source is not None else None), - } - for t in self._tools.values() - ], - } - - @pydantic.model_validator(mode="wrap") - @classmethod - def _validate( - cls, - data: Any, - handler: pydantic.ValidatorFunctionWrapHandler, - ) -> Context: - """Reconstruct from serialized form or pass through normal init. - - When deserializing, tools are schema-only (not executable) - unless their sources can be resolved from the global registry. - """ - # Normal construction (already a Context, or keyword args without - # a ``tools`` key that looks like serialized tool dicts). - if isinstance(data, cls): - return data - if not isinstance(data, dict) or "tools" not in data: - result: Context = handler(data) - return result - - # Check whether tools contains serialized dicts (from model_dump) - # vs. live Tool objects (from normal __init__). - tools_value = data["tools"] - if tools_value and isinstance(tools_value[0], dict): - return cls._from_serialized(data) - - # Live Tool objects — let the normal init path handle it. - result = handler(data) - return result - - @classmethod - def _from_serialized(cls, data: dict[str, Any]) -> Context: - """Reconstruct from ``model_dump()`` output.""" - from . import tools as tools_ - - ctx = cls( - system_prompt=data.get("system_prompt", ""), - messages=[ - messages_.Message.model_validate(m) for m in data.get("messages", []) - ], - ) - - for tool_data in data.get("tools", []): - schema = tools_.ToolSchema.model_validate(tool_data["schema"]) - source_data = tool_data.get("source") - source = ToolSource(**source_data) if source_data else None - - # Try to resolve the tool from the global registry - live_tool = tools_.get_tool(schema.name) - if live_tool is not None: - ctx.register_tool(live_tool) - else: - # Schema-only placeholder — inspectable but not executable - placeholder = tools_.Tool( - fn=tools_._unresolvable_tool_fn(schema.name), - schema=schema, - source=source, - ) - ctx.register_tool(placeholder) - - return ctx - - -# ── Contextvar ──────────────────────────────────────────────────── - -_context: contextvars.ContextVar[Context] = contextvars.ContextVar("context") - - -def get_context() -> Context: - """Get the active Context from the current run. - - Raises ``LookupError`` if called outside of ``ai.run()``. - """ - return _context.get() diff --git a/src/vercel_ai_sdk/agents3/durability.py b/src/vercel_ai_sdk/agents/durability.py similarity index 100% rename from src/vercel_ai_sdk/agents3/durability.py rename to src/vercel_ai_sdk/agents/durability.py diff --git a/src/vercel_ai_sdk/agents/hooks.py b/src/vercel_ai_sdk/agents/hooks.py index 948539bb..0ab850e8 100644 --- a/src/vercel_ai_sdk/agents/hooks.py +++ b/src/vercel_ai_sdk/agents/hooks.py @@ -1,245 +1,268 @@ +"""Hooks: suspension points that require external input to continue. + +Usage inside an agent loop:: + + result = await hook("approve_delete", payload=ToolApproval, metadata={"tool": "rm"}) + if result.granted: + ... + +Resolution from outside the loop:: + + resolve_hook("approve_delete", {"granted": True}) + +Cancellation:: + + await cancel_hook("approve_delete", reason="denied") + +Behavior depends on ``interrupt_loop``: + +interrupt_loop=False (default, long-running): the await blocks until +resolve_hook() is called from outside (e.g. websocket handler, API endpoint). + +interrupt_loop=True (serverless): if no resolution is available, the +hook's future is cancelled. The branch receives CancelledError and dies +cleanly. On re-entry, call resolve_hook() before agent.run() to +pre-register the resolution, then pass checkpoint= to replay. +""" + from __future__ import annotations import asyncio -from typing import TYPE_CHECKING, Any, ClassVar +from typing import Any import pydantic +from .. import _durability as _dctx from ..types import messages as messages_ - -if TYPE_CHECKING: - from . import runtime as runtime_ - +from . import runtime as runtime_ # --------------------------------------------------------------------------- # Module-level hook registries # # _live_hooks: -# Populated by Hook.create() when a hook suspends inside a running graph. +# Populated by hook() when it suspends inside a running agent. # Maps hook label -> (future, metadata dict, Runtime). -# Consumed by Hook.resolve() / Hook.cancel() to unblock the awaiting +# Consumed by resolve_hook() / cancel_hook() to unblock the awaiting # coroutine. Entries are removed when the hook resolves, cancels, or # the run completes. # # _pending_resolutions: -# Populated by Hook.resolve() when no live hook exists yet (serverless -# re-entry: the user calls resolve() *before* ai.run() replays the graph). -# Maps hook label -> validated resolution dict. -# Consumed by Hook.create() at the start of graph execution — if a -# pre-registered resolution exists for the label, the hook returns -# immediately without suspending. Entries are removed on consumption. +# Populated by resolve_hook() when no live hook exists yet (serverless +# re-entry: the user calls resolve_hook() *before* agent.run() replays). +# Maps hook label -> (payload type, validated resolution dict). +# Consumed by hook() at the start of execution — if a pre-registered +# resolution exists for the label, the hook returns immediately without +# suspending. Entries are removed on consumption. # --------------------------------------------------------------------------- _live_hooks: dict[ - str, tuple[asyncio.Future[Any], dict[str, Any], runtime_.Runtime] + str, tuple[asyncio.Future[dict[str, Any]], dict[str, Any], runtime_.Runtime] ] = {} _pending_resolutions: dict[str, dict[str, Any]] = {} -# label -> validated resolution dict -def _cleanup_run(labels: set[str]) -> None: +def cleanup_run(labels: set[str]) -> None: """Remove all registry entries associated with a finished run.""" for label in labels: _live_hooks.pop(label, None) _pending_resolutions.pop(label, None) -class Hook[T: pydantic.BaseModel]: - """Hook: a suspension point that requires external input to continue. - - Usage in graph code: - - approval = await ToolApproval.create("approve_delete", metadata={...}) - if approval.granted: - ... - - Resolution from outside the graph: - - ToolApproval.resolve("approve_delete", {"granted": True, ...}) - - Behavior depends on the ``cancels_future`` class variable: - - cancels_future=False (default, long-running): the await blocks until - Hook.resolve() is called from outside the graph (e.g., websocket - handler, API endpoint). - - cancels_future=True (serverless): if no resolution is available, the - hook's future is cancelled by run(). The branch receives CancelledError - and dies cleanly. On re-entry, call Hook.resolve() before ai.run() to - pre-register the resolution, then pass checkpoint= to replay. +async def hook[T: pydantic.BaseModel]( + label: str, + *, + payload: type[T], + metadata: dict[str, Any] | None = None, + interrupt_loop: bool = False, +) -> T: + """Create a hook suspension point and await its resolution. + + Args: + label: Unique identifier for this hook instance. + payload: Pydantic model class — the resolution data must validate + against this type. The return value is a validated instance. + metadata: Arbitrary metadata surfaced in the pending signal message + and checkpoint. Useful for UI rendering (e.g. which tool needs + approval, what arguments it received). + interrupt_loop: When ``True`` (serverless mode), the hook's future + is cancelled if no resolution is available, causing + ``CancelledError`` in the awaiting coroutine. When ``False`` + (long-running mode), the future is held until resolved + externally. """ - - _schema: ClassVar[type[pydantic.BaseModel]] - hook_type: ClassVar[str] - cancels_future: ClassVar[bool] = False - - @classmethod - async def create(cls, label: str, metadata: dict[str, Any] | None = None) -> T: - """Create a hook and await its resolution. - - The hook is submitted to the LoopExecutor's step queue. run() will - either: - - Resolve immediately (if a resolution is available from checkpoint - or pre-registered via Hook.resolve()) - - Cancel the future (cancels_future=True, serverless mode) - - Hold the future (cancels_future=False, long-running mode) - """ - from . import runtime as rt_mod - - rt = rt_mod._runtime.get(None) - if rt is None: - raise ValueError("No Runtime context - must be called within ai.run()") - - # Check pre-registered resolutions (serverless re-entry path) - pre_registered = _pending_resolutions.pop(label, None) - if pre_registered is not None: - rt.log.record_hook(label, pre_registered) - return cls._schema(**pre_registered) # type: ignore[return-value] - - # Check checkpoint for a previously resolved value - resolution = rt.log.get_hook_resolution(label) - if resolution is not None: - rt.log.record_hook(label, resolution) - return cls._schema(**resolution) # type: ignore[return-value] - - # Submit to executor queue — run() decides what to do - future: asyncio.Future[dict[str, Any]] = asyncio.Future() - suspension = rt_mod.HookSuspension( - label=label, - hook_type=cls.hook_type, - metadata=metadata or {}, - future=future, - cancels_future=cls.cancels_future, + rt = runtime_.get_runtime() + hook_metadata = metadata or {} + + provider = _dctx.get_provider() + + # Path 1: pre-registered resolution (serverless re-entry). + pre_registered = _pending_resolutions.pop(label, None) + if pre_registered is not None: + if provider is not None: + provider.record_hook(label, pre_registered) + return payload(**pre_registered) + + # Path 2: cached resolution from checkpoint (durability replay). + if provider is not None: + cached = provider.get_hook_resolution(label) + if cached is not None: + provider.record_hook(label, cached) + return payload(**cached) + + # Path 3: no resolution available — suspend. + future: asyncio.Future[dict[str, Any]] = asyncio.Future() + + _live_hooks[label] = (future, hook_metadata, rt) + rt.track_hook_label(label) + + # Emit pending signal message. + await rt.put_message( + messages_.Message( + role="signal", + parts=[ + messages_.HookPart( + hook_id=label, + hook_type=payload.__name__, + status="pending", + metadata=hook_metadata, + ) + ], ) - await rt.executor.put_hook(suspension) + ) - # Register in module-level registry for external resolution - hook_metadata = metadata or {} - _live_hooks[label] = (future, hook_metadata, rt) - rt.executor.track_hook_label(label) + if interrupt_loop: + # Yield control so the consumer can see the pending message, + # then cancel — the caller catches CancelledError. + await asyncio.sleep(0) + if not future.done(): + future.cancel() + + # Await resolution — may be resolved externally or cancelled. + resolution = await future + + # Clean up live registry. + _live_hooks.pop(label, None) + + # Record for checkpoint. + if provider is not None: + provider.record_hook(label, resolution) + + # Emit resolved signal message. + await rt.put_message( + messages_.Message( + role="signal", + parts=[ + messages_.HookPart( + hook_id=label, + hook_type=payload.__name__, + status="resolved", + metadata=hook_metadata, + resolution=resolution, + ) + ], + ) + ) - # Await resolution — may be resolved immediately by run(), - # cancelled by run() (serverless), or resolved later by - # Hook.resolve() (long-running). - resolution = await future + return payload(**resolution) - # Clean up - _live_hooks.pop(label, None) - # Record for checkpoint - rt.log.record_hook(label, resolution) - - # Emit resolved message - await rt.executor.put_message( - messages_.Message( - role="assistant", - parts=[ - messages_.HookPart( - hook_id=label, - hook_type=cls.hook_type, - status="resolved", - metadata=hook_metadata, - resolution=resolution, - ) - ], - ) - ) +def resolve_hook( + label: str, + data: pydantic.BaseModel | dict[str, Any], + *, + payload: type[pydantic.BaseModel] | None = None, +) -> None: + """Resolve a hook by label. - return cls._schema(**resolution) # type: ignore[return-value] + Works in two modes: - @classmethod - def resolve(cls, label: str, data: T | dict[str, Any]) -> None: - """Resolve a hook by label. + 1. **Live hook exists** (long-running): validates data (if ``payload`` + type is provided), resolves the future immediately, unblocking the + awaiting coroutine. - Works in two modes: + 2. **No live hook yet** (serverless re-entry): stashes the resolution + in the pre-registration registry. When ``hook()`` executes during + replay, it finds the pre-registered value and returns without + suspending. - 1. Live hook exists (long-running): validates data, resolves the - future immediately, unblocking the awaiting coroutine. - - 2. No live hook yet (serverless re-entry): validates data and - stashes it in the pre-registration registry. When ai.run() - replays the graph and Hook.create() executes, it finds the - pre-registered resolution and returns without suspending. - """ - # Validate and normalize to dict - if isinstance(data, dict): - validated = cls._schema(**data) + Args: + label: The hook label to resolve. + data: Resolution data — a dict or pydantic model instance. + payload: Optional pydantic model class for validation. When + omitted and *data* is a model instance, its type is used. + """ + # Normalize to dict. + if isinstance(data, pydantic.BaseModel): + resolution = data.model_dump() + elif isinstance(data, dict): + if payload is not None: + # Validate against the payload type. + validated = payload(**data) resolution = validated.model_dump() else: - if not isinstance(data, cls._schema): - raise TypeError( - f"Expected {cls._schema.__name__} or dict, " - f"got {type(data).__name__}" - ) - resolution = data.model_dump() - - # Path 1: live hook — resolve the future directly - if label in _live_hooks: - future, _, _rt = _live_hooks[label] - future.set_result(resolution) - return - - # Path 2: no live hook — pre-register for later consumption - _pending_resolutions[label] = resolution - - @classmethod - async def cancel(cls, label: str, reason: str | None = None) -> None: - """Cancel a pending hook. - - Only works for live hooks (long-running mode). Raises if the - hook is not currently pending. - """ - if label not in _live_hooks: - raise ValueError(f"No pending hook with label: {label}") - - future, hook_metadata, rt = _live_hooks.pop(label) - future.cancel(reason) - - await rt.executor.put_message( - messages_.Message( - role="assistant", - parts=[ - messages_.HookPart( - hook_id=label, - hook_type=cls.hook_type, - status="cancelled", - metadata=hook_metadata, - ) - ], - ) - ) + resolution = data + else: + raise TypeError(f"Expected dict or pydantic model, got {type(data).__name__}") + + # Path 1: live hook — resolve the future directly. + if label in _live_hooks: + future, _, _rt = _live_hooks[label] + future.set_result(resolution) + return + # Path 2: no live hook — pre-register for later consumption. + _pending_resolutions[label] = resolution -def hook[T: pydantic.BaseModel](cls: type[T]) -> type[Hook[T]]: - """Decorator to create a Hook type from a pydantic model. - The pydantic model defines the schema for the hook's resolution payload. +async def cancel_hook(label: str, *, reason: str | None = None) -> None: + """Cancel a pending hook. + + Only works for live hooks (long-running mode). Raises ValueError + if the hook is not currently pending. """ - hook_impl = type( - cls.__name__, - (Hook,), - { - "_schema": cls, - "hook_type": cls.__name__, - "cancels_future": cls.__dict__.get("cancels_future", False), - "__doc__": cls.__doc__, - }, + if label not in _live_hooks: + raise ValueError(f"No pending hook with label: {label!r}") + + future, hook_metadata, rt = _live_hooks.pop(label) + future.cancel(reason) + + # Emit cancelled signal message. + await rt.put_message( + messages_.Message( + role="signal", + parts=[ + messages_.HookPart( + hook_id=label, + hook_type="", # not available at cancel site + status="cancelled", + metadata=hook_metadata, + ) + ], + ) ) - return hook_impl + +# ── Built-in hook payloads ──────────────────────────────────────── -@hook class ToolApproval(pydantic.BaseModel): - """Prewired hook for tool call approval. + """Payload schema for tool-approval hooks. - Used by the AI SDK UI adapter to bridge the protocol's - tool-approval-request / approval-responded flow to the - hook system. - """ + Usage inside a loop:: - cancels_future: ClassVar[bool] = True + approval = await hook( + f"approve_{tc.tool_call_id}", + payload=ToolApproval, + metadata={"tool_name": tc.tool_name}, + interrupt_loop=True, + ) + if approval.granted: + ... + """ granted: bool reason: str | None = None + + +TOOL_APPROVAL_HOOK_TYPE = ToolApproval.__name__ diff --git a/src/vercel_ai_sdk/agents/mcp/__init__.py b/src/vercel_ai_sdk/agents/mcp/__init__.py index c1202f63..873e7008 100644 --- a/src/vercel_ai_sdk/agents/mcp/__init__.py +++ b/src/vercel_ai_sdk/agents/mcp/__init__.py @@ -1,6 +1,7 @@ -from .client import get_http_tools, get_stdio_tools +from .client import close_connections, get_http_tools, get_stdio_tools __all__ = [ "get_stdio_tools", "get_http_tools", + "close_connections", ] diff --git a/src/vercel_ai_sdk/agents/mcp/client.py b/src/vercel_ai_sdk/agents/mcp/client.py index 17d7bb3a..c42f0d9e 100644 --- a/src/vercel_ai_sdk/agents/mcp/client.py +++ b/src/vercel_ai_sdk/agents/mcp/client.py @@ -14,7 +14,8 @@ import mcp.client.streamable_http import mcp.types -from .. import context, tools +from ... import types +from ..agent import Tool as Tool_ __all__ = [ "get_stdio_tools", @@ -31,8 +32,7 @@ class _Connection: exit_stack: contextlib.AsyncExitStack -# Connection pool stored in contextvar, scoped to execute() -# The pool is set by execute() and cleaned up when execute() finishes +# Connection pool stored in contextvar, scoped to Agent.run() _pool: contextvars.ContextVar[dict[str, _Connection] | None] = contextvars.ContextVar( "mcp_connections", default=None ) @@ -49,7 +49,7 @@ async def _get_or_create_connection( if pool is None: raise RuntimeError( - "MCP tools must be used inside ai.execute(). " + "MCP tools must be used inside agent.run(). " "The connection pool is not initialized." ) @@ -57,17 +57,12 @@ async def _get_or_create_connection( if key in pool: return pool[key].client - # Use AsyncExitStack for clean resource management exit_stack = contextlib.AsyncExitStack() try: - # Enter the transport context streams = await exit_stack.enter_async_context(transport_factory()) - - # Handle both (read, write) and (read, write, callback) returns read_stream, write_stream = streams[0], streams[1] - # Create and initialize the client session client = mcp.client.session.ClientSession( read_stream=read_stream, write_stream=write_stream, @@ -79,7 +74,6 @@ async def _get_or_create_connection( return client except BaseException: - # Clean up on any error during setup await exit_stack.aclose() raise @@ -103,7 +97,6 @@ async def call_tool(**kwargs: Any) -> Any: f"MCP tool call timed out after 30 seconds: {tool_name}" ) from e - # Handle error responses if result.isError: error_text = " ".join( part.text @@ -112,15 +105,12 @@ async def call_tool(**kwargs: Any) -> Any: ) raise RuntimeError(f"MCP tool error: {error_text or 'Unknown error'}") - # Prefer structured content if available if result.structuredContent is not None: return result.structuredContent - # Fall back to parsing content for part in result.content: if isinstance(part, mcp.types.TextContent): text = part.text - # Try to parse JSON, otherwise return raw text if text.startswith(("{", "[")): try: return json.loads(text) @@ -139,12 +129,11 @@ async def get_stdio_tools( env: dict[str, str] | None = None, cwd: str | None = None, tool_prefix: str | None = None, -) -> list[tools.Tool[..., Any]]: - """ - Get tools from an MCP server running as a subprocess. +) -> list[Tool_[..., Any]]: + """Get tools from an MCP server running as a subprocess. Connection is managed automatically - created on first use, cleaned up - when execute() finishes. + when the agent run finishes. Args: command: The command to run (e.g., "npx", "python"). @@ -156,7 +145,8 @@ async def get_stdio_tools( Returns: List of Tool objects that can be passed to an agent or custom loop. - Example: + Example:: + tools = await ai.mcp.get_stdio_tools( "npx", "-y", "@anthropic/mcp-server-filesystem", "/tmp" ) @@ -187,12 +177,11 @@ async def get_http_tools( *, headers: dict[str, str] | None = None, tool_prefix: str | None = None, -) -> list[tools.Tool[..., Any]]: - """ - Get tools from an MCP server over HTTP (Streamable HTTP transport). +) -> list[Tool_[..., Any]]: + """Get tools from an MCP server over HTTP (Streamable HTTP transport). Connection is managed automatically - created on first use, cleaned up - when execute() finishes. + when the agent run finishes. Args: url: The URL of the MCP server endpoint. @@ -202,7 +191,8 @@ async def get_http_tools( Returns: List of Tool objects that can be passed to an agent or custom loop. - Example: + Example:: + tools = await ai.mcp.get_http_tools( "http://localhost:3000/mcp", headers={"Authorization": "Bearer xxx"} @@ -230,53 +220,30 @@ def _mcp_tool_to_native( connection_key: str, transport_factory: Callable[[], contextlib.AbstractAsyncContextManager[Any]], tool_prefix: str | None, -) -> tools.Tool[..., Any]: +) -> Tool_[..., Any]: """Convert an MCP tool to a native Tool.""" name = mcp_tool.name if tool_prefix: name = f"{tool_prefix}_{name}" - schema = tools.ToolSchema( + schema = types.ToolSchema( name=name, description=mcp_tool.description or "", param_schema=mcp_tool.inputSchema, return_type=Any, ) - # Determine source provenance from connection key - if connection_key.startswith("http:"): - source = context.ToolSource( - kind="mcp_http", - uri=connection_key.removeprefix("http:"), - ) - elif connection_key.startswith("stdio:"): - source = context.ToolSource( - kind="mcp_stdio", - server_command=connection_key.removeprefix("stdio:"), - ) - else: - source = context.ToolSource(kind="mcp") - - t = tools.Tool( + return Tool_( fn=_make_tool_fn(connection_key, mcp_tool.name, transport_factory), schema=schema, - source=source, ) - # Register on active Context if available, else fall back to global - ctx = context._context.get(None) - if ctx is not None: - ctx.register_tool(t) - tools._tool_registry[name] = t - return t - async def close_connections() -> None: - """ - Close all MCP connections in the current context. + """Close all MCP connections in the current context. - This is called automatically by execute(), but can be called - manually for explicit cleanup. + Called automatically at the end of an agent run, but can also be + called manually for explicit cleanup. """ pool = _pool.get() if pool is None: @@ -286,7 +253,6 @@ async def close_connections() -> None: if not pool: return - # Use TaskGroup for concurrent cleanup async with asyncio.TaskGroup() as tg: for conn in pool.values(): tg.create_task(_close_connection_safely(conn)) diff --git a/src/vercel_ai_sdk/agents/runtime.py b/src/vercel_ai_sdk/agents/runtime.py index 8362adc0..45e69b77 100644 --- a/src/vercel_ai_sdk/agents/runtime.py +++ b/src/vercel_ai_sdk/agents/runtime.py @@ -1,150 +1,17 @@ +"""Runtime: message sink that connects producer coroutines to the consumer.""" + from __future__ import annotations import asyncio import contextvars -import dataclasses -import json -import logging -from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine -from typing import Any, get_type_hints - -import pydantic +from collections.abc import AsyncGenerator, AsyncIterable, Awaitable +from .. import types from ..telemetry import events as telemetry -from ..types import messages as messages_ -from . import checkpoint as checkpoint_ -from . import context as context_ -from . import hooks, mcp, streams, tools - -logger = logging.getLogger(__name__) - - -# ── EventLog ────────────────────────────────────────────────────── -# -# Pure bookkeeping: replay from checkpoint + record new events. -# No asyncio, no queues — just data in, data out. -# - - -class EventLog: - """Replay/record layer backed by a Checkpoint. - - Holds replay cursors (read pointer into the checkpoint) and - recording lists (new events produced during this run). - Completely synchronous — no queues, no async. - """ - - def __init__(self, checkpoint: checkpoint_.Checkpoint | None = None) -> None: - self._checkpoint = checkpoint or checkpoint_.Checkpoint() - - # Replay cursors - self._step_index: int = 0 - self._tool_replay: dict[str, checkpoint_.ToolEvent] = { - t.tool_call_id: t for t in self._checkpoint.tools - } - self._hook_replay: dict[str, dict[str, Any]] = { - h.label: h.resolution for h in self._checkpoint.hooks - } - - # Recording lists (new events from this run) - self._step_log: list[checkpoint_.StepEvent] = [] - self._tool_log: list[checkpoint_.ToolEvent] = [] - self._hook_log: list[checkpoint_.HookEvent] = [] - - # ── Steps ───────────────────────────────────────────────── - - @property - def step_index(self) -> int: - return self._step_index - - def try_replay_step(self) -> streams.StreamResult | None: - if self._step_index < len(self._checkpoint.steps): - event = self._checkpoint.steps[self._step_index] - self._step_index += 1 - logger.info("Replaying step %d from checkpoint", event.index) - return event.to_stream_result() - return None - - def record_step(self, result: streams.StreamResult) -> None: - event = checkpoint_.StepEvent( - index=self._step_index, - messages=list(result.messages), - ) - self._step_log.append(event) - self._step_index += 1 - - # ── Tools ───────────────────────────────────────────────── - - def try_replay_tool(self, tool_call_id: str) -> checkpoint_.ToolEvent | None: - event = self._tool_replay.get(tool_call_id) - if event is not None: - logger.info( - "Replaying tool %s (call_id=%s) from checkpoint", - event.tool_call_id, - tool_call_id, - ) - return event - - def record_tool( - self, tool_call_id: str, result: Any, *, status: str = "result" - ) -> None: - self._tool_log.append( - checkpoint_.ToolEvent( - tool_call_id=tool_call_id, result=result, status=status - ) - ) - - # ── Hooks ───────────────────────────────────────────────── - - def get_hook_resolution(self, label: str) -> dict[str, Any] | None: - resolution = self._hook_replay.get(label) - if resolution is not None: - logger.info("Resolving hook '%s' from checkpoint", label) - return resolution - - def record_hook(self, label: str, resolution: dict[str, Any]) -> None: - self._hook_log.append(checkpoint_.HookEvent(label=label, resolution=resolution)) - - # ── Snapshot ────────────────────────────────────────────── - - def checkpoint( - self, pending_hooks: list[checkpoint_.PendingHookInfo] | None = None - ) -> checkpoint_.Checkpoint: - """Build a full Checkpoint merging prior state + new recordings.""" - return checkpoint_.Checkpoint( - steps=list(self._checkpoint.steps) + self._step_log, - tools=list(self._checkpoint.tools) + self._tool_log, - hooks=list(self._checkpoint.hooks) + self._hook_log, - pending_hooks=pending_hooks or [], - ) - - -# ── LoopExecutor ───────────────────────────────────────────────── -# -# Async coordination: queues that let graph code (streams, hooks, -# tools) talk to the driver loop. Pure mailbox — no replay, no -# checkpoint awareness. -# - -@dataclasses.dataclass -class HookSuspension: - """Submitted to the step queue when a hook needs resolution.""" - label: str - hook_type: str - metadata: dict[str, Any] - future: asyncio.Future[Any] - cancels_future: bool = False - - -class LoopExecutor: - """Async coordination layer between graph code and the driver loop. - - Graph code (``@stream`` decorators, hooks, tool execution) submits - work via the producer methods. The driver loop consumes via - ``next()`` and ``drain_messages()``. - """ +class Runtime: + """Central message queue. Producers put messages, run() yields them.""" class _Sentinel: pass @@ -152,466 +19,93 @@ class _Sentinel: _SENTINEL = _Sentinel() def __init__(self) -> None: - self._step_queue: asyncio.Queue[ - tuple[streams.Stream, asyncio.Future[streams.StreamResult]] - | HookSuspension - | LoopExecutor._Sentinel - ] = asyncio.Queue() - - self._message_queue: asyncio.Queue[messages_.Message] = asyncio.Queue() - - # Pending hooks (unresolved during this run) - self._pending_hooks: dict[str, HookSuspension] = {} - - # Track hook labels registered in this run for cleanup + self._message_queue: asyncio.Queue[types.Message | Runtime._Sentinel] = ( + asyncio.Queue() + ) self._hook_labels: set[str] = set() - # ── Producers (called by graph code) ────────────────────── - - async def put_step( - self, step_fn: streams.Stream, future: asyncio.Future[streams.StreamResult] - ) -> None: - await self._step_queue.put((step_fn, future)) - - async def put_hook(self, suspension: HookSuspension) -> None: - await self._step_queue.put(suspension) - - async def put_message(self, message: messages_.Message) -> None: + async def put_message(self, message: types.Message) -> None: await self._message_queue.put(message) - async def done(self) -> None: - await self._step_queue.put(self._SENTINEL) - - # ── Consumer (called by driver loop) ────────────────────── - - async def next( - self, timeout: float = 0.1 - ) -> ( - tuple[streams.Stream, asyncio.Future[streams.StreamResult]] - | HookSuspension - | None - ): - """Pull the next item from the step queue. - - Returns ``None`` on timeout (no item available). - Returns the sentinel's semantic equivalent by raising StopIteration - when the graph signals completion. - """ - try: - item = await asyncio.wait_for(self._step_queue.get(), timeout=timeout) - except TimeoutError: - return None - - if isinstance(item, LoopExecutor._Sentinel): - raise _LoopDone - return item - - def drain_messages(self) -> list[messages_.Message]: - msgs: list[messages_.Message] = [] - while not self._message_queue.empty(): - try: - msgs.append(self._message_queue.get_nowait()) - except asyncio.QueueEmpty: - break - return msgs - - # ── Hook label tracking ─────────────────────────────────── + async def signal_done(self) -> None: + await self._message_queue.put(self._SENTINEL) def track_hook_label(self, label: str) -> None: + """Register a hook label for cleanup when the run ends.""" self._hook_labels.add(label) - def pending_hook_infos(self) -> list[checkpoint_.PendingHookInfo]: - return [ - checkpoint_.PendingHookInfo( - label=sus.label, - hook_type=sus.hook_type, - metadata=sus.metadata, - ) - for sus in self._pending_hooks.values() - ] - + def cleanup_hooks(self) -> None: + """Remove all hook registry entries for this run.""" + from . import hooks as hooks_ -class _LoopDone(Exception): - """Internal signal: the loop function has finished.""" - - -# ── Runtime ─────────────────────────────────────────────────────── -# -# Thin composition of EventLog + LoopExecutor. -# The context var points here; graph code accesses rt.log and -# rt.executor directly. -# - - -class Runtime: - """Central coordinator — composes EventLog and LoopExecutor. - - Graph code accesses ``rt.log`` for replay/record and - ``rt.executor`` for async coordination. - """ - - def __init__(self, checkpoint: checkpoint_.Checkpoint | None = None) -> None: - self.log = EventLog(checkpoint) - self.executor = LoopExecutor() - - def checkpoint(self) -> checkpoint_.Checkpoint: - return self.log.checkpoint( - pending_hooks=self.executor.pending_hook_infos(), - ) + hooks_.cleanup_run(self._hook_labels) + self._hook_labels.clear() _runtime: contextvars.ContextVar[Runtime] = contextvars.ContextVar("runtime") -def get_checkpoint() -> checkpoint_.Checkpoint: - """Get the current checkpoint from the active Runtime.""" - return _runtime.get().checkpoint() +def get_runtime() -> Runtime: + """Return the active Runtime. Raises LookupError outside of run().""" + return _runtime.get() -def _find_runtime_param(fn: Callable[..., Any]) -> str | None: - """Find a parameter typed as Runtime, return its name or None.""" - try: - hints = get_type_hints(fn) - except Exception: - return None - for name, hint in hints.items(): - if hint is Runtime: - return name - return None - - -async def execute_tool( - tool_call: messages_.ToolCallPart, - message: messages_.Message | None = None, -) -> messages_.ToolResultPart: - """Execute a single tool call with replay support. - - Looks up the tool by name — first from the active Context (if any), - then from the global registry. Returns a :class:`ToolResultPart` - with the result. - - If a checkpoint exists with a cached result for this tool_call_id, - returns the cached result without re-executing. - """ - rt = _runtime.get(None) - - # Replay: return result part from cache - if rt: - cached = rt.log.try_replay_tool(tool_call.tool_call_id) - if cached is not None: - return messages_.ToolResultPart( - tool_call_id=tool_call.tool_call_id, - tool_name=tool_call.tool_name, - result=cached.result, - is_error=cached.status == "error", - ) - - telemetry.handle( - telemetry.ToolCallStartEvent( - tool_name=tool_call.tool_name, - tool_call_id=tool_call.tool_call_id, - args=tool_call.tool_args, - ) - ) - t0 = telemetry.time_ms() - - # Fresh execution — resolve from Context first, then global registry - tool: tools.Tool[..., Any] | None = None - ctx = context_._context.get(None) - if ctx is not None: - tool = ctx.get_tool(tool_call.tool_name) - if tool is None: - tool = tools.get_tool(tool_call.tool_name) - if tool is None: - raise ValueError(f"Tool not found in registry: {tool_call.tool_name}") - - error_str: str | None = None - is_error = False +async def _stop_when_done(runtime: Runtime, task: Awaitable[None]) -> None: try: - result = await tool.validate_and_call(tool_call.tool_args, rt) - except (json.JSONDecodeError, pydantic.ValidationError) as exc: - result = f"{type(exc).__name__}: {exc}" - error_str = result - is_error = True - - result_part = messages_.ToolResultPart( - tool_call_id=tool_call.tool_call_id, - tool_name=tool_call.tool_name, - result=result, - is_error=is_error, - ) - - telemetry.handle( - telemetry.ToolCallFinishEvent( - tool_name=tool_call.tool_name, - tool_call_id=tool_call.tool_call_id, - result=result, - error=error_str, - duration_ms=telemetry.time_ms() - t0, - ) - ) - - # Record for checkpoint - status = "error" if is_error else "result" - if rt: - rt.log.record_tool(tool_call.tool_call_id, result, status=status) - - # Emit tool result message so UI sees the result - if rt and message: - tool_msg = messages_.Message( - role="tool", - parts=[result_part], - ) - await rt.executor.put_message(tool_msg) - - return result_part - + await task + finally: + await runtime.signal_done() -# ── RunResult ───────────────────────────────────────────────────── +async def run( + source: AsyncIterable[types.Message], +) -> AsyncGenerator[types.Message]: + """Run *source* and yield every message that gets put into the Runtime queue.""" + from .mcp import client as mcp_client -@dataclasses.dataclass -class HookInfo: - """Info about a pending (unresolved) hook, exposed on RunResult.""" + rt = Runtime() + token = _runtime.set(rt) - label: str - hook_type: str - metadata: dict[str, Any] + # MCP connection pool — scoped to this run. + mcp_pool: dict[str, mcp_client._Connection] = {} + mcp_token = mcp_client._pool.set(mcp_pool) + token_run_id = telemetry.start_run() + telemetry.handle(telemetry.RunStartEvent()) -class RunResult: - """Returned by run(). Async-iterate for messages, then check state. + run_error: BaseException | None = None + total_usage: types.Usage | None = None - Usage: - result = ai.run(my_graph, llm, query) - async for msg in result: - ... - result.checkpoint # Checkpoint with all completed work - result.pending_hooks # dict of unresolved hooks (empty if graph completed) - """ + async def _drain() -> None: + async for message in source: + await rt.put_message(message) - def __init__(self) -> None: - self._messages: AsyncGenerator[messages_.Message] | None = None - self._runtime: Runtime | None = None - - @property - def checkpoint(self) -> checkpoint_.Checkpoint: - if self._runtime is None: - return checkpoint_.Checkpoint() - return self._runtime.checkpoint() - - @property - def pending_hooks(self) -> dict[str, HookInfo]: - if self._runtime is None: - return {} - return { - label: HookInfo( - label=sus.label, - hook_type=sus.hook_type, - metadata=sus.metadata, - ) - for label, sus in self._runtime.executor._pending_hooks.items() - } - - async def __aiter__(self) -> AsyncGenerator[messages_.Message]: - if self._messages is not None: - async for msg in self._messages: - yield msg - - -# ── run() ───────────────────────────────────────────────────────── - - -async def _stop_when_done(executor: LoopExecutor, task: Awaitable[None]) -> None: try: - await task - finally: - await executor.done() - - -def run( - root: Callable[..., Coroutine[Any, Any, Any]], - *args: Any, - checkpoint: checkpoint_.Checkpoint | None = None, - context: context_.Context | None = None, -) -> RunResult: - """Main entry point. - - 1. Starts the root function as a background task - 2. Pulls steps and hook suspensions from the LoopExecutor queue - 3. Executes each step, yielding messages - 4. Resolves or suspends hooks depending on the hook's cancels_future - 5. Returns RunResult with .checkpoint and .pending_hooks - - Args: - root: The loop function to execute. - *args: Positional arguments forwarded to ``root``. - checkpoint: Checkpoint to resume from. - context: LLM prompt context (tools, system prompt, messages). - If ``None``, an empty Context is created automatically. - """ - result = RunResult() - - # Discard stale checkpoints: if the checkpoint has pending hooks but - # none of them have been resolved, this isn't a resume. - effective_checkpoint = checkpoint - if checkpoint and checkpoint.pending_hooks: - pending_labels = [ph.label for ph in checkpoint.pending_hooks] - has_resolution = any( - label in hooks._pending_resolutions for label in pending_labels - ) - if not has_resolution: - logger.info( - "Discarding stale checkpoint: pending hooks %s have no " - "matching resolutions", - pending_labels, - ) - effective_checkpoint = None - else: - logger.info( - "Resuming from checkpoint with %d pending hook(s): %s", - len(pending_labels), - pending_labels, - ) - - async def _generate() -> AsyncGenerator[messages_.Message]: - rt = Runtime(checkpoint=effective_checkpoint) - result._runtime = rt - token_runtime = _runtime.set(rt) - - ctx = context or context_.Context() - token_context = context_._context.set(ctx) - - token_run_id = telemetry.start_run() - - telemetry.handle(telemetry.RunStartEvent()) - - mcp_pool: dict[str, mcp.client._Connection] = {} - mcp_token = mcp.client._pool.set(mcp_pool) - - kwargs: dict[str, Any] = {} - if runtime_param := _find_runtime_param(root): - kwargs[runtime_param] = rt - - run_error: BaseException | None = None - total_usage: messages_.Usage | None = None - - try: - async with asyncio.TaskGroup() as tg: - _task: asyncio.Task[None] = tg.create_task( - _stop_when_done(rt.executor, root(*args, **kwargs)) - ) - - while True: - # Drain pending messages - for msg in rt.executor.drain_messages(): - yield msg - - # Pull next item from the graph executor - try: - item = await rt.executor.next() - except _LoopDone: - for msg in rt.executor.drain_messages(): - yield msg - break - - if item is None: - # Timeout — no item available, loop again - continue - - # ── Hook suspension ──────────────────────── - if isinstance(item, HookSuspension): - resolution = rt.log.get_hook_resolution(item.label) - if resolution is not None: - item.future.set_result(resolution) - rt.log.record_hook(item.label, resolution) - else: - rt.executor._pending_hooks[item.label] = item - if item.cancels_future: - item.future.cancel() - - yield messages_.Message( - role="assistant", - parts=[ - messages_.HookPart( - hook_id=item.label, - hook_type=item.hook_type, - status="pending", - metadata=item.metadata, - ) - ], - ) - - await asyncio.sleep(0) - for msg in rt.executor.drain_messages(): - yield msg - continue - - # ── Regular step ─────────────────────────── - step_fn, future = item - - telemetry.handle( - telemetry.StepStartEvent( - step_index=rt.log.step_index, - ) + async with asyncio.TaskGroup() as tg: + tg.create_task(_stop_when_done(rt, _drain())) + + while True: + item = await rt._message_queue.get() + if isinstance(item, Runtime._Sentinel): + return + # Track usage from done messages. + if item.is_done and item.usage is not None: + total_usage = ( + item.usage if total_usage is None else total_usage + item.usage ) + yield item - for tool_msg in rt.executor.drain_messages(): - yield tool_msg - - result_messages: list[messages_.Message] = [] + except BaseException as exc: + run_error = exc + raise - async for msg in step_fn(): - yield msg - result_messages.append(msg) - - for tool_msg in rt.executor.drain_messages(): - yield tool_msg + finally: + telemetry.handle(telemetry.RunFinishEvent(usage=total_usage, error=run_error)) + telemetry.end_run(token_run_id) - step_result = streams.StreamResult(messages=result_messages) - future.set_result(step_result) + rt.cleanup_hooks() - telemetry.handle( - telemetry.StepFinishEvent( - step_index=rt.log.step_index, - result=step_result, - ) - ) + await mcp_client.close_connections() + mcp_client._pool.reset(mcp_token) - # Accumulate usage for run-level telemetry - step_usage = step_result.usage - if step_usage is not None: - total_usage = ( - step_usage - if total_usage is None - else total_usage + step_usage - ) - - await asyncio.sleep(0) - for tool_msg in rt.executor.drain_messages(): - yield tool_msg - - except BaseException as exc: - run_error = exc - raise - - finally: - telemetry.handle( - telemetry.RunFinishEvent( - usage=total_usage, - error=run_error, - ) - ) - telemetry.end_run(token_run_id) - - hooks._cleanup_run(rt.executor._hook_labels) - - if mcp_token is not None: - await mcp.client.close_connections() - mcp.client._pool.reset(mcp_token) - - context_._context.reset(token_context) - _runtime.reset(token_runtime) - - result._messages = _generate() - return result + _runtime.reset(token) diff --git a/src/vercel_ai_sdk/agents/streams.py b/src/vercel_ai_sdk/agents/streams.py deleted file mode 100644 index 095ed995..00000000 --- a/src/vercel_ai_sdk/agents/streams.py +++ /dev/null @@ -1,105 +0,0 @@ -from __future__ import annotations - -import asyncio -import dataclasses -import functools -from collections.abc import AsyncGenerator, Awaitable, Callable -from typing import Any - -from ..types import messages as messages_ - - -@dataclasses.dataclass -class StreamResult: - messages: list[messages_.Message] = dataclasses.field(default_factory=list) - - @property - def last_message(self) -> messages_.Message | None: - return self.messages[-1] if self.messages else None - - @property - def tool_calls(self) -> list[messages_.ToolCallPart]: - """Get tool calls from the last message.""" - if self.last_message: - return self.last_message.tool_calls - return [] - - @property - def text(self) -> str: - if self.last_message: - return self.last_message.text - return "" - - @property - def output(self) -> Any: - """Parsed structured output from the last message, if available.""" - if self.last_message: - return self.last_message.output - return None - - @property - def usage(self) -> messages_.Usage | None: - """Token usage from the last (most recent) LLM call.""" - if self.last_message: - return self.last_message.usage - return None - - @property - def total_usage(self) -> messages_.Usage | None: - """Accumulated token usage across all LLM calls in this result. - - Sums usage from every message that carries it (i.e. assistant - messages produced by LLM calls). Returns ``None`` if no message - reported usage. - """ - total: messages_.Usage | None = None - for msg in self.messages: - if msg.usage is not None: - total = msg.usage if total is None else total + msg.usage - return total - - -Stream = Callable[[], AsyncGenerator[messages_.Message]] -# maybe it should have a name and an id inferred from LLM outputs - - -def stream[**P]( - fn: Callable[P, AsyncGenerator[messages_.Message]], -) -> Callable[P, Awaitable[StreamResult]]: - """Decorator to put an async generator into the LoopExecutor queue. - - The decorated function submits its work to the executor queue and - blocks until run() processes it, then returns the StreamResult. - - If a checkpoint exists with a cached result for this step index, - returns the cached result without submitting to the queue (replay). - """ - - from . import runtime as runtime_ - - @functools.wraps(fn) - async def wrapped(*args: Any, **kwargs: Any) -> StreamResult: - rt: runtime_.Runtime | None = runtime_._runtime.get(None) - if rt is None: - raise ValueError("No Runtime context - must be called within ai.run()") - - # Replay: return cached result if available - cached = rt.log.try_replay_step() - if cached is not None: - return cached - - # Fresh execution: submit to executor queue and wait - future: asyncio.Future[StreamResult] = asyncio.Future() - - async def stream_fn() -> AsyncGenerator[messages_.Message]: - async for msg in fn(*args, **kwargs): - yield msg - - await rt.executor.put_step(stream_fn, future) - result = await future - - # Record for checkpoint - rt.log.record_step(result) - return result - - return wrapped diff --git a/src/vercel_ai_sdk/agents/tools.py b/src/vercel_ai_sdk/agents/tools.py deleted file mode 100644 index 39a9aa28..00000000 --- a/src/vercel_ai_sdk/agents/tools.py +++ /dev/null @@ -1,136 +0,0 @@ -from __future__ import annotations - -import inspect -import json -from collections.abc import Awaitable, Callable -from typing import TYPE_CHECKING, Any, get_type_hints - -import pydantic - -from ..types.tools import ToolLike as ToolLike -from ..types.tools import ToolSchema as ToolSchema -from .context import ToolSource - -if TYPE_CHECKING: - from . import runtime as runtime_ - -# Module-level tool registry - populated at decoration time -_tool_registry: dict[str, Tool[..., Any]] = {} - - -def get_tool(name: str) -> Tool[..., Any] | None: - """Look up a tool by name from the global registry.""" - return _tool_registry.get(name) - - -def _is_runtime_type(hint: Any) -> bool: - """Check if a type hint is the Runtime class.""" - # Import here to avoid circular import at runtime - from .runtime import Runtime - - return hint is Runtime - - -class Tool[**P, R]: - def __init__( - self, - fn: Callable[P, Awaitable[R]], - schema: ToolSchema, - validator: type[pydantic.BaseModel] | None = None, - source: ToolSource | None = None, - ) -> None: - self._fn = fn - self._validator = validator - self.schema = schema - self.source = source - - async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: - return await self._fn(*args, **kwargs) - - async def validate_and_call( - self, json_str: str, runtime: runtime_.Runtime | None - ) -> R: - from .runtime import _find_runtime_param - - kwargs = json.loads(json_str) if json_str else {} - - if runtime and (rt_param := _find_runtime_param(self._fn)): - kwargs[rt_param] = runtime - - # validate llm-generated inputs (skipped for MCP tools) - if self._validator is not None: - self._validator.model_validate(kwargs) - return await self._fn(**kwargs) # type: ignore[call-arg] - - @property - def name(self) -> str: - return self.schema.name - - @property - def description(self) -> str: - return self.schema.description - - @property - def param_schema(self) -> dict[str, Any]: - return self.schema.param_schema - - -def tool[**P, R](fn: Callable[P, Awaitable[R]]) -> Tool[P, R]: - """Decorator to define a tool from an async function.""" - - # 1. build tool schema by parsing the function - sig = inspect.signature(fn) - hints = get_type_hints(fn) if hasattr(fn, "__annotations__") else {} - - fields: dict[str, Any] = {} - - for param_name, param in sig.parameters.items(): - param_type = hints.get(param_name, str) - - if _is_runtime_type(param_type): - continue - if param.default is inspect.Parameter.empty: - fields[param_name] = (param_type, ...) - else: - fields[param_name] = (param_type, param.default) - - validator = pydantic.create_model(f"{fn.__name__}_Args", **fields) - - # 2. instantiate the tool - - schema = ToolSchema( - name=fn.__name__, - description=inspect.getdoc(fn) or "", - param_schema=validator.model_json_schema(), - return_type=hints.get("return", None), - ) - - source = ToolSource( - kind="python", - module=getattr(fn, "__module__", None), - qualname=getattr(fn, "__qualname__", None), - ) - - t = Tool(fn=fn, schema=schema, validator=validator, source=source) - - # 3. register in global registry - _tool_registry[t.name] = t - return t - - -def _unresolvable_tool_fn(name: str) -> Callable[..., Awaitable[Any]]: - """Create a placeholder async function for schema-only tools. - - Used by ``Context.from_dict()`` when a tool's source cannot be - resolved to live code. - """ - - async def _placeholder(**kwargs: Any) -> Any: - raise RuntimeError( - f"Tool {name!r} was reconstructed from serialized context " - f"and has no executable implementation." - ) - - _placeholder.__name__ = name - _placeholder.__qualname__ = name - return _placeholder diff --git a/src/vercel_ai_sdk/agents3/__init__.py b/src/vercel_ai_sdk/agents3/__init__.py deleted file mode 100644 index 6b440140..00000000 --- a/src/vercel_ai_sdk/agents3/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -from .agent import Agent, Context, Tool, ToolCall, agent, tool -from .checkpoint import Checkpoint, HookEvent, PendingHookInfo, StepEvent, ToolEvent -from .durability import DurabilityProvider, EventLogProvider -from .hooks import cancel_hook, hook, resolve_hook - -__all__ = [ - "Agent", - "Checkpoint", - "Context", - "DurabilityProvider", - "EventLogProvider", - "HookEvent", - "PendingHookInfo", - "StepEvent", - "Tool", - "ToolCall", - "ToolEvent", - "agent", - "cancel_hook", - "hook", - "resolve_hook", - "tool", -] diff --git a/src/vercel_ai_sdk/agents3/agent.py b/src/vercel_ai_sdk/agents3/agent.py deleted file mode 100644 index 616931e0..00000000 --- a/src/vercel_ai_sdk/agents3/agent.py +++ /dev/null @@ -1,276 +0,0 @@ -"""Agent, Context, StreamResult, and the stream() function.""" - -from __future__ import annotations - -import asyncio -import inspect -import json -from collections.abc import AsyncGenerator, Awaitable, Callable -from typing import Any, Protocol, get_type_hints - -import pydantic - -from .. import _durability as _dctx -from .. import models, types -from ..types import builders -from . import checkpoint as checkpoint_ -from . import durability as durability_ -from . import runtime - - -class Tool[**P, R]: - """Wraps async function, introspects schema, attaches a validator""" - - def __init__( - self, - fn: Callable[P, Awaitable[R]], - schema: types.ToolSchema, - validator: type[pydantic.BaseModel] | None = None, - *, - is_gen: bool = False, - ) -> None: - self._fn = fn - self._validator = validator - self._is_gen = is_gen - self.schema = schema - - async def __call__(self, json_args: str) -> R: - """Parse json_args into kwargs, validate, and call the function.""" - kwargs = json.loads(json_args) if json_args else {} - if self._validator is not None: - self._validator.model_validate(kwargs) - - if not self._is_gen: - return await self._fn(**kwargs) - - # Generator tool (e.g. agent-as-a-tool): drain the async - # generator, pipe each yielded message to the runtime for - # real-time streaming, and return the final text as the result. - rt = runtime.get_runtime() - last: types.Message | None = None - async for message in self._fn(**kwargs): - await rt.put_message(message) - last = message - return last.text if last else "" - - @property - def name(self) -> str: - return self.schema.name - - @property - def description(self) -> str: - return self.schema.description - - @property - def param_schema(self) -> dict[str, Any]: - return self.schema.param_schema - - -def tool[**P, R](fn: Callable[P, Awaitable[R]]) -> Tool[P, R]: - """Decorator: turn an async function into a :class:`Tool`.""" - sig = inspect.signature(fn) - hints = get_type_hints(fn) if hasattr(fn, "__annotations__") else {} - - fields: dict[str, Any] = {} - for param_name, param in sig.parameters.items(): - param_type = hints.get(param_name, str) - if param.default is inspect.Parameter.empty: - fields[param_name] = (param_type, ...) - else: - fields[param_name] = (param_type, param.default) - - validator = pydantic.create_model(f"{fn.__name__}_Args", **fields) - - schema = types.ToolSchema( - name=fn.__name__, - description=inspect.getdoc(fn) or "", - param_schema=validator.model_json_schema(), - return_type=hints.get("return", None), - ) - - return Tool( - fn=fn, - schema=schema, - validator=validator, - is_gen=inspect.isasyncgenfunction(fn), - ) - - -class ToolCall: - """Callable that binds a :class:`ToolCallPart` to its :class:`Tool`. - - Calling it executes the tool and returns a :class:`ToolResultPart`. - """ - - def __init__(self, part: types.ToolCallPart, tool: Tool[..., Any]) -> None: - self._part = part - self._tool = tool - - @property - def id(self) -> str: - return self._part.tool_call_id - - @property - def name(self) -> str: - return self._part.tool_name - - @property - def args(self) -> str: - return self._part.tool_args - - async def __call__(self) -> types.ToolResultPart: - """Execute the tool and return a :class:`ToolResultPart`. - - If a durability provider is active, the call is routed through - it for recording or replay. - """ - provider = _dctx.get_provider() - - async def _execute() -> types.ToolResultPart: - try: - result = await self._tool(self._part.tool_args) - except Exception as exc: - return types.ToolResultPart( - tool_call_id=self._part.tool_call_id, - tool_name=self._part.tool_name, - result=str(exc), - is_error=True, - ) - return types.ToolResultPart( - tool_call_id=self._part.tool_call_id, - tool_name=self._part.tool_name, - result=result, - ) - - if provider is not None: - return await provider.execute_tool( - _execute, - tool_call_id=self.id, - tool_name=self.name, - ) - return await _execute() - - -class Context(pydantic.BaseModel): - """Everything that goes into the LLM.""" - - model: models.Model - messages: list[types.Message] - tools: list[Tool[..., Any]] - - model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) - - def resolve(self, tool_parts: list[types.ToolCallPart]) -> list[ToolCall]: - """Resolve ToolCallParts into callable ToolCall objects.""" - tools_by_name = {t.name: t for t in self.tools} - return [ - ToolCall(part=tp, tool=tools_by_name[tp.tool_name]) for tp in tool_parts - ] - - -class LoopFn(Protocol): - def __call__(self, context: Context) -> AsyncGenerator[types.Message]: ... - - -async def _default_loop(context: Context) -> AsyncGenerator[types.Message]: - while True: - stream = await models.stream( - context.model, context.messages, tools=context.tools - ) - async for message in stream: - yield message - - tool_calls = context.resolve(stream.tool_calls) - if not tool_calls: - break - - # Execute tool calls in parallel. - async with asyncio.TaskGroup() as tg: - tasks = [tg.create_task(tc()) for tc in tool_calls] - - # Yield a tool-result message — history auto-collects it. - yield builders.tool_message(*(t.result() for t in tasks)) - - -async def _collect_messages( - source: AsyncGenerator[types.Message], - messages: list[types.Message], -) -> AsyncGenerator[types.Message]: - """Intercept yielded messages and collect done ones into *messages*. - - This runs on the **producer** side (same coroutine as the loop function), - so ``messages`` is always up-to-date by the time the loop reads it for - the next model call — avoiding the race that would occur if collection - happened on the consumer side of the runtime queue. - """ - async for message in source: - if message.is_done: - for i, existing in enumerate(messages): - if existing.id == message.id: - messages[i] = message - break - else: - messages.append(message) - yield message - - -class Agent: - """Bag of configuration: model + tools + loop.""" - - def __init__( - self, - *, - tools: list[Tool[..., Any]] | None = None, - ) -> None: - self._tools: list[Tool[..., Any]] = tools or [] - self._loop_fn: LoopFn = _default_loop - - def loop(self, fn: LoopFn) -> LoopFn: - """Decorator: override the default loop function.""" - self._loop_fn = fn - return fn - - async def run( - self, - model: models.Model, - messages: list[types.Message], - *, - durability: durability_.DurabilityProvider | None = None, - checkpoint: checkpoint_.Checkpoint | None = None, - ) -> AsyncGenerator[types.Message]: - """Run the agent loop, yielding messages to the consumer. - - Args: - model: The model to use for LLM calls. - messages: Initial conversation messages. - durability: Explicit durability provider. If ``None`` but - *checkpoint* is given, an :class:`EventLogProvider` is - created automatically. - checkpoint: Checkpoint to resume from. Implies eventlog - durability when no explicit *durability* is provided. - """ - # Convenience: checkpoint implies eventlog provider. - if checkpoint is not None and durability is None: - durability = durability_.EventLogProvider(checkpoint) - - context = Context(model=model, messages=list(messages), tools=self._tools) - - # Set the durability provider on the shared context var so that - # models.stream() and ToolCall.__call__() auto-detect it. - token = _dctx.set_provider(durability) if durability is not None else None - try: - source = _collect_messages(self._loop_fn(context), context.messages) - async for message in runtime.run(source): - yield message - finally: - if token is not None: - _dctx.reset_provider(token) - - -def agent( - *, - tools: list[Tool[..., Any]] | None = None, - system: str | None = None, -) -> Agent: - """Create an Agent.""" - return Agent(tools=tools) diff --git a/src/vercel_ai_sdk/agents3/checkpoint.py b/src/vercel_ai_sdk/agents3/checkpoint.py deleted file mode 100644 index 6cd0d96e..00000000 --- a/src/vercel_ai_sdk/agents3/checkpoint.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Checkpoint data model for durable agent execution. - -A Checkpoint is a serializable snapshot of all completed work in an agent -run. On re-entry, the durability provider replays cached results from the -checkpoint instead of re-executing LLM calls and tool invocations. -""" - -from __future__ import annotations - -from typing import Any - -import pydantic - -from ..types import messages as messages_ - - -class StepEvent(pydantic.BaseModel): - """A completed LLM stream step — stores the final done message.""" - - index: int - message: messages_.Message - - -class ToolEvent(pydantic.BaseModel): - """A completed tool execution.""" - - tool_call_id: str - tool_name: str - result: Any - status: str = "result" # "result" | "error" - - -class HookEvent(pydantic.BaseModel): - """A resolved hook.""" - - label: str - resolution: dict[str, Any] - - -class PendingHookInfo(pydantic.BaseModel): - """A hook that was suspended but not resolved when the run ended.""" - - label: str - payload_type: str # fully qualified name of the pydantic model - metadata: dict[str, Any] = {} - - -class Checkpoint(pydantic.BaseModel): - """Serializable snapshot of all completed work in an agent run.""" - - steps: list[StepEvent] = [] - tools: list[ToolEvent] = [] - hooks: list[HookEvent] = [] - pending_hooks: list[PendingHookInfo] = [] diff --git a/src/vercel_ai_sdk/agents3/hooks.py b/src/vercel_ai_sdk/agents3/hooks.py deleted file mode 100644 index 09c92911..00000000 --- a/src/vercel_ai_sdk/agents3/hooks.py +++ /dev/null @@ -1,243 +0,0 @@ -"""Hooks: suspension points that require external input to continue. - -Usage inside an agent loop:: - - result = await hook("approve_delete", payload=ToolApproval, metadata={"tool": "rm"}) - if result.granted: - ... - -Resolution from outside the loop:: - - resolve_hook("approve_delete", {"granted": True}) - -Cancellation:: - - await cancel_hook("approve_delete", reason="denied") - -Behavior depends on ``interrupt_loop``: - -interrupt_loop=False (default, long-running): the await blocks until -resolve_hook() is called from outside (e.g. websocket handler, API endpoint). - -interrupt_loop=True (serverless): if no resolution is available, the -hook's future is cancelled. The branch receives CancelledError and dies -cleanly. On re-entry, call resolve_hook() before agent.run() to -pre-register the resolution, then pass checkpoint= to replay. -""" - -from __future__ import annotations - -import asyncio -from typing import Any - -import pydantic - -from .. import _durability as _dctx -from ..types import messages as messages_ -from . import runtime as runtime_ - -# --------------------------------------------------------------------------- -# Module-level hook registries -# -# _live_hooks: -# Populated by hook() when it suspends inside a running agent. -# Maps hook label -> (future, metadata dict, Runtime). -# Consumed by resolve_hook() / cancel_hook() to unblock the awaiting -# coroutine. Entries are removed when the hook resolves, cancels, or -# the run completes. -# -# _pending_resolutions: -# Populated by resolve_hook() when no live hook exists yet (serverless -# re-entry: the user calls resolve_hook() *before* agent.run() replays). -# Maps hook label -> (payload type, validated resolution dict). -# Consumed by hook() at the start of execution — if a pre-registered -# resolution exists for the label, the hook returns immediately without -# suspending. Entries are removed on consumption. -# --------------------------------------------------------------------------- - -_live_hooks: dict[ - str, tuple[asyncio.Future[dict[str, Any]], dict[str, Any], runtime_.Runtime] -] = {} - -_pending_resolutions: dict[str, dict[str, Any]] = {} - - -def cleanup_run(labels: set[str]) -> None: - """Remove all registry entries associated with a finished run.""" - for label in labels: - _live_hooks.pop(label, None) - _pending_resolutions.pop(label, None) - - -async def hook[T: pydantic.BaseModel]( - label: str, - *, - payload: type[T], - metadata: dict[str, Any] | None = None, - interrupt_loop: bool = False, -) -> T: - """Create a hook suspension point and await its resolution. - - Args: - label: Unique identifier for this hook instance. - payload: Pydantic model class — the resolution data must validate - against this type. The return value is a validated instance. - metadata: Arbitrary metadata surfaced in the pending signal message - and checkpoint. Useful for UI rendering (e.g. which tool needs - approval, what arguments it received). - interrupt_loop: When ``True`` (serverless mode), the hook's future - is cancelled if no resolution is available, causing - ``CancelledError`` in the awaiting coroutine. When ``False`` - (long-running mode), the future is held until resolved - externally. - """ - rt = runtime_.get_runtime() - hook_metadata = metadata or {} - - provider = _dctx.get_provider() - - # Path 1: pre-registered resolution (serverless re-entry). - pre_registered = _pending_resolutions.pop(label, None) - if pre_registered is not None: - if provider is not None: - provider.record_hook(label, pre_registered) - return payload(**pre_registered) - - # Path 2: cached resolution from checkpoint (durability replay). - if provider is not None: - cached = provider.get_hook_resolution(label) - if cached is not None: - provider.record_hook(label, cached) - return payload(**cached) - - # Path 3: no resolution available — suspend. - future: asyncio.Future[dict[str, Any]] = asyncio.Future() - - _live_hooks[label] = (future, hook_metadata, rt) - rt.track_hook_label(label) - - # Emit pending signal message. - await rt.put_message( - messages_.Message( - role="signal", - parts=[ - messages_.HookPart( - hook_id=label, - hook_type=payload.__name__, - status="pending", - metadata=hook_metadata, - ) - ], - ) - ) - - if interrupt_loop: - # Yield control so the consumer can see the pending message, - # then cancel — the caller catches CancelledError. - await asyncio.sleep(0) - if not future.done(): - future.cancel() - - # Await resolution — may be resolved externally or cancelled. - resolution = await future - - # Clean up live registry. - _live_hooks.pop(label, None) - - # Record for checkpoint. - if provider is not None: - provider.record_hook(label, resolution) - - # Emit resolved signal message. - await rt.put_message( - messages_.Message( - role="signal", - parts=[ - messages_.HookPart( - hook_id=label, - hook_type=payload.__name__, - status="resolved", - metadata=hook_metadata, - resolution=resolution, - ) - ], - ) - ) - - return payload(**resolution) - - -def resolve_hook( - label: str, - data: pydantic.BaseModel | dict[str, Any], - *, - payload: type[pydantic.BaseModel] | None = None, -) -> None: - """Resolve a hook by label. - - Works in two modes: - - 1. **Live hook exists** (long-running): validates data (if ``payload`` - type is provided), resolves the future immediately, unblocking the - awaiting coroutine. - - 2. **No live hook yet** (serverless re-entry): stashes the resolution - in the pre-registration registry. When ``hook()`` executes during - replay, it finds the pre-registered value and returns without - suspending. - - Args: - label: The hook label to resolve. - data: Resolution data — a dict or pydantic model instance. - payload: Optional pydantic model class for validation. When - omitted and *data* is a model instance, its type is used. - """ - # Normalize to dict. - if isinstance(data, pydantic.BaseModel): - resolution = data.model_dump() - elif isinstance(data, dict): - if payload is not None: - # Validate against the payload type. - validated = payload(**data) - resolution = validated.model_dump() - else: - resolution = data - else: - raise TypeError(f"Expected dict or pydantic model, got {type(data).__name__}") - - # Path 1: live hook — resolve the future directly. - if label in _live_hooks: - future, _, _rt = _live_hooks[label] - future.set_result(resolution) - return - - # Path 2: no live hook — pre-register for later consumption. - _pending_resolutions[label] = resolution - - -async def cancel_hook(label: str, *, reason: str | None = None) -> None: - """Cancel a pending hook. - - Only works for live hooks (long-running mode). Raises ValueError - if the hook is not currently pending. - """ - if label not in _live_hooks: - raise ValueError(f"No pending hook with label: {label!r}") - - future, hook_metadata, rt = _live_hooks.pop(label) - future.cancel(reason) - - # Emit cancelled signal message. - await rt.put_message( - messages_.Message( - role="signal", - parts=[ - messages_.HookPart( - hook_id=label, - hook_type="", # not available at cancel site - status="cancelled", - metadata=hook_metadata, - ) - ], - ) - ) diff --git a/src/vercel_ai_sdk/agents3/runtime.py b/src/vercel_ai_sdk/agents3/runtime.py deleted file mode 100644 index 484f3ed8..00000000 --- a/src/vercel_ai_sdk/agents3/runtime.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Runtime: message sink that connects producer coroutines to the consumer.""" - -from __future__ import annotations - -import asyncio -import contextvars -from collections.abc import AsyncGenerator, AsyncIterable, Awaitable - -from .. import types - - -class Runtime: - """Central message queue. Producers put messages, run() yields them.""" - - class _Sentinel: - pass - - _SENTINEL = _Sentinel() - - def __init__(self) -> None: - self._message_queue: asyncio.Queue[types.Message | Runtime._Sentinel] = ( - asyncio.Queue() - ) - self._hook_labels: set[str] = set() - - async def put_message(self, message: types.Message) -> None: - await self._message_queue.put(message) - - async def signal_done(self) -> None: - await self._message_queue.put(self._SENTINEL) - - def track_hook_label(self, label: str) -> None: - """Register a hook label for cleanup when the run ends.""" - self._hook_labels.add(label) - - def cleanup_hooks(self) -> None: - """Remove all hook registry entries for this run.""" - from . import hooks as hooks_ - - hooks_.cleanup_run(self._hook_labels) - self._hook_labels.clear() - - -_runtime: contextvars.ContextVar[Runtime] = contextvars.ContextVar("runtime") - - -def get_runtime() -> Runtime: - """Return the active Runtime. Raises LookupError outside of run().""" - return _runtime.get() - - -async def _stop_when_done(runtime: Runtime, task: Awaitable[None]) -> None: - try: - await task - finally: - await runtime.signal_done() - - -async def run( - source: AsyncIterable[types.Message], -) -> AsyncGenerator[types.Message]: - """Run *source* and yield every message that gets put into the Runtime queue.""" - - runtime = Runtime() - token = _runtime.set(runtime) - - async def _drain() -> None: - async for message in source: - await runtime.put_message(message) - - try: - async with asyncio.TaskGroup() as tg: - tg.create_task(_stop_when_done(runtime, _drain())) - - while True: - item = await runtime._message_queue.get() - if isinstance(item, Runtime._Sentinel): - return - yield item - - finally: - runtime.cleanup_hooks() - _runtime.reset(token) diff --git a/src/vercel_ai_sdk/models/__init__.py b/src/vercel_ai_sdk/models/__init__.py index a36236a3..d6bc2d4e 100644 --- a/src/vercel_ai_sdk/models/__init__.py +++ b/src/vercel_ai_sdk/models/__init__.py @@ -169,7 +169,7 @@ async def stream( the final ``Message``. After iteration, access ``.text``, ``.tool_calls``, ``.usage``, etc. - If a :class:`~vercel_ai_sdk.agents3.durability.DurabilityProvider` is + If a :class:`~vercel_ai_sdk.agents.durability.DurabilityProvider` is active (set by ``Agent.run()``), the stream is routed through the provider for recording or replay. """ diff --git a/src/vercel_ai_sdk/telemetry/events.py b/src/vercel_ai_sdk/telemetry/events.py index 3064ad9f..bf631523 100644 --- a/src/vercel_ai_sdk/telemetry/events.py +++ b/src/vercel_ai_sdk/telemetry/events.py @@ -29,7 +29,6 @@ def handle(self, event: ai.telemetry.TelemetryEvent) -> None: import uuid from typing import Any, Protocol, runtime_checkable -from ..agents import streams from ..types import messages as messages_ # ── Protocol ─────────────────────────────────────────────────────── @@ -66,10 +65,10 @@ class StepStartEvent(TelemetryEvent): @dataclasses.dataclass(frozen=True, slots=True) class StepFinishEvent(TelemetryEvent): - """Emitted when a ``@stream``-decorated step finishes.""" + """Emitted when an LLM stream step finishes.""" step_index: int - result: streams.StreamResult + message: messages_.Message @dataclasses.dataclass(frozen=True, slots=True) diff --git a/src/vercel_ai_sdk/telemetry/otel.py b/src/vercel_ai_sdk/telemetry/otel.py index d4f72a00..4e327b07 100644 --- a/src/vercel_ai_sdk/telemetry/otel.py +++ b/src/vercel_ai_sdk/telemetry/otel.py @@ -232,20 +232,21 @@ def _on_step_finish(self, event: StepFinishEvent) -> None: return span, _ctx = entry + msg = event.message attrs: dict[str, Any] = {} - usage = event.result.usage + usage = msg.usage if usage: attrs["gen_ai.usage.input_tokens"] = usage.input_tokens attrs["gen_ai.usage.output_tokens"] = usage.output_tokens - text = event.result.text + text = msg.text if text: attrs["ai.response.text"] = {"output": lambda: text} finish_reason = None - if event.result.last_message and not event.result.tool_calls: + if not msg.tool_calls: finish_reason = "stop" - elif event.result.tool_calls: + elif msg.tool_calls: finish_reason = "tool_calls" if finish_reason: attrs["ai.response.finishReason"] = finish_reason diff --git a/src/vercel_ai_sdk/types/tools.py b/src/vercel_ai_sdk/types/tools.py index 01cee703..0661ff29 100644 --- a/src/vercel_ai_sdk/types/tools.py +++ b/src/vercel_ai_sdk/types/tools.py @@ -1,7 +1,7 @@ """Tool schema types — what the LLM layer sees. These are schema-only definitions used by LanguageModel.stream(tools=...). -The executable Tool class and @tool decorator live in agents.tools. +The executable Tool class and @tool decorator live in agents.agent. """ from __future__ import annotations diff --git a/tests/adapters/ai_sdk_ui/test_adapter.py b/tests/adapters/ai_sdk_ui/test_adapter.py index cc50c1e6..1b1e4519 100644 --- a/tests/adapters/ai_sdk_ui/test_adapter.py +++ b/tests/adapters/ai_sdk_ui/test_adapter.py @@ -247,11 +247,7 @@ async def test_runtime_tool_roundtrip() -> None: status="pending", but pydantic models are mutable so when we collect them at the end, we see the mutated state. """ - weather_agent = ai.agent( - model=MOCK_MODEL, - system="You are helpful.", - tools=[get_weather], - ) + weather_agent = ai.agent(tools=[get_weather]) # First LLM call: returns a tool call tool_call_response = [ @@ -283,7 +279,7 @@ async def test_runtime_tool_roundtrip() -> None: # Collect all messages from the runtime runtime_messages: list[messages.Message] = [] async for msg in weather_agent.run( - ai.make_messages(user="What's the weather in London?") + MOCK_MODEL, ai.make_messages(user="What's the weather in London?") ): runtime_messages.append(msg) @@ -556,7 +552,7 @@ async def test_tool_approval_hook_emits_approval_request() -> None: parts=[ messages.HookPart( hook_id="approve_tc-1", - hook_type=hooks.ToolApproval.hook_type, # type: ignore[attr-defined] + hook_type=hooks.TOOL_APPROVAL_HOOK_TYPE, status="pending", metadata={"tool_name": "rm_rf", "tool_args": '{"path": "/"}'}, ), @@ -617,51 +613,51 @@ def test_approval_responded_resolves_hook() -> None: async def test_runtime_tool_approval_same_step() -> None: """E2E: tool-approval-request must land in the same SSE step as the tool call. - Runs a graph with ToolApproval (cancels_future=True) through ai.run(), + Runs a graph with ToolApproval (interrupt_loop=True) through agent.run(), collects runtime messages, streams through the adapter, and asserts that no spurious step boundary appears between tool-input-available and tool-approval-request. - - This is the test that would have caught the bug where the Runtime's - HookPart message (which has a different id from the LLM message) - caused the adapter to open a new step. """ + from collections.abc import AsyncGenerator as AG @ai.tool async def dangerous_action(path: str) -> str: """Do something dangerous.""" return f"deleted {path}" - approval_agent = ai.agent( - model=MOCK_MODEL, - system="You are helpful.", - tools=[dangerous_action], - ) + approval_agent = ai.agent(tools=[dangerous_action]) @approval_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: - result = await ai.stream_step(agent.model, msgs, agent.tools) - if not result.tool_calls: + async def custom(context: ai.Context) -> AG[ai.Message]: + stream = await ai.models.stream( + context.model, context.messages, tools=context.tools + ) + async for msg in stream: + yield msg + + tool_calls = context.resolve(stream.tool_calls) + if not tool_calls: return - last_msg = result.last_message - assert last_msg is not None - - async def approve_and_execute(tc: ai.ToolCallPart) -> ai.ToolResultPart: - approval = await ai.ToolApproval.create( # type: ignore[attr-defined] - f"approve_{tc.tool_call_id}", - metadata={"tool_name": tc.tool_name}, + async def approve_and_execute(tc: ai.ToolCall) -> ai.ToolResultPart: + approval = await ai.hook( + f"approve_{tc.id}", + payload=ai.ToolApproval, + metadata={"tool_name": tc.name}, + interrupt_loop=True, ) if approval.granted: - return await ai.execute_tool(tc, message=last_msg) + return await tc() return ai.ToolResultPart( - tool_call_id=tc.tool_call_id, - tool_name=tc.tool_name, + tool_call_id=tc.id, + tool_name=tc.name, result="denied", is_error=True, ) - await asyncio.gather(*(approve_and_execute(tc) for tc in result.tool_calls)) + async with asyncio.TaskGroup() as tg: + tasks = [tg.create_task(approve_and_execute(tc)) for tc in tool_calls] + yield ai.tool_message(*(t.result() for t in tasks)) mock_llm( [ @@ -676,13 +672,11 @@ async def approve_and_execute(tc: ai.ToolCallPart) -> ai.ToolResultPart: ) runtime_messages: list[messages.Message] = [] - result = approval_agent.run(ai.make_messages(user="delete /tmp")) - async for msg in result: + async for msg in approval_agent.run( + MOCK_MODEL, ai.make_messages(user="delete /tmp") + ): runtime_messages.append(msg) - # The run should have a pending hook (approval not yet granted) - assert "approve_tc-1" in result.pending_hooks - # Stream through UI adapter event_types = [ p.type @@ -690,10 +684,6 @@ async def approve_and_execute(tc: ai.ToolCallPart) -> ai.ToolResultPart: ] # tool-approval-request must be in the SAME step as tool-input. - # If a spurious step boundary sneaks in, we'd see: - # [..., "tool-input-available", "finish-step", "start-step", - # "tool-approval-request", ...] - # which breaks the frontend's sendAutomaticallyWhen helper. assert event_types == [ "start", "start-step", diff --git a/tests/agents/mcp/test_client.py b/tests/agents/mcp/test_client.py index 243df91d..7c4fc2a1 100644 --- a/tests/agents/mcp/test_client.py +++ b/tests/agents/mcp/test_client.py @@ -1,4 +1,4 @@ -"""MCP client: tool registration in global registry, end-to-end execution.""" +"""MCP client: tool conversion, end-to-end execution.""" import contextlib from typing import Any @@ -8,7 +8,6 @@ import vercel_ai_sdk as ai from vercel_ai_sdk.agents.mcp.client import _mcp_tool_to_native -from vercel_ai_sdk.agents.tools import _tool_registry, get_tool from ...conftest import MOCK_MODEL, mock_llm, text_msg, tool_call_msg @@ -33,26 +32,24 @@ def _noop_transport_factory() -> contextlib.AbstractAsyncContextManager[Any]: raise NotImplementedError("should not be called") -# -- _mcp_tool_to_native registers in global registry ---------------------- +# -- _mcp_tool_to_native produces a valid Tool ---------------------------- -def test_mcp_tool_to_native_registers_in_global_registry() -> None: - """Converting an MCP tool to native registers it in _tool_registry.""" - mcp_tool = _fake_mcp_tool(name="mcp_reg_test") +def test_mcp_tool_to_native_basic() -> None: + """Converting an MCP tool to native produces a Tool with correct schema.""" + mcp_tool = _fake_mcp_tool(name="mcp_basic_test") native = _mcp_tool_to_native(mcp_tool, "test:key", _noop_transport_factory, None) - assert native.name == "mcp_reg_test" - assert get_tool("mcp_reg_test") is native - assert _tool_registry["mcp_reg_test"] is native + assert native.name == "mcp_basic_test" + assert native.description == "Echo input" def test_mcp_tool_to_native_with_prefix() -> None: - """Tool prefix is prepended to the name and both name forms are correct.""" + """Tool prefix is prepended to the name.""" mcp_tool = _fake_mcp_tool(name="echo") native = _mcp_tool_to_native(mcp_tool, "test:key", _noop_transport_factory, "ctx7") assert native.name == "ctx7_echo" - assert get_tool("ctx7_echo") is native def test_mcp_tool_to_native_schema_preserved() -> None: @@ -76,15 +73,14 @@ async def fake_fn(**kwargs: str) -> str: call_log.append(kwargs) return f"echoed: {kwargs.get('text', '')}" - # Build and register a tool the same way the MCP client does, + # Build a tool the same way the MCP client does, # but with a fake fn so we don't need a real MCP server. mcp_tool = _fake_mcp_tool(name="mcp_e2e_echo") native = _mcp_tool_to_native(mcp_tool, "test:key", _noop_transport_factory, None) - # Replace the real fn (which would try to connect) with our fake + # Replace the real fn (which would try to connect) with our fake. native._fn = fake_fn - _tool_registry[native.name] = native - my_agent = ai.agent(model=MOCK_MODEL, tools=[native]) + my_agent = ai.agent(tools=[native]) call1 = [ tool_call_msg(tc_id="tc-mcp-1", name="mcp_e2e_echo", args='{"text": "hello"}') @@ -92,17 +88,18 @@ async def fake_fn(**kwargs: str) -> str: call2 = [text_msg("Done.", id="msg-2")] llm = mock_llm([call1, call2]) - result = my_agent.run(ai.make_messages(user="echo hello")) - msgs = [m async for m in result] + msgs: list[ai.Message] = [] + async for msg in my_agent.run(MOCK_MODEL, ai.make_messages(user="echo hello")): + msgs.append(msg) - # Tool was called with the right args + # Tool was called with the right args. assert len(call_log) == 1 assert call_log[0] == {"text": "hello"} - # Tool result is visible in messages + # Tool result is visible in messages. tool_results = [m for m in msgs if m.role == "tool" and m.tool_results] assert len(tool_results) >= 1 assert tool_results[0].tool_results[0].result == "echoed: hello" - # LLM was called twice (tool call + final text) + # LLM was called twice (tool call + final text). assert llm.call_count == 2 diff --git a/tests/agents/test_checkpoint.py b/tests/agents/test_checkpoint.py index 4c240017..4f2f24dc 100644 --- a/tests/agents/test_checkpoint.py +++ b/tests/agents/test_checkpoint.py @@ -1,7 +1,6 @@ """Checkpoint replay, hook cancellation/resolution, serialization.""" -import asyncio -from typing import Any, ClassVar +from collections.abc import AsyncGenerator import pydantic import pytest @@ -12,9 +11,7 @@ from ..conftest import MOCK_MODEL, mock_llm, text_msg, tool_call_msg -@ai.hook class Approval(pydantic.BaseModel): - cancels_future: ClassVar[bool] = True granted: bool @@ -23,21 +20,28 @@ class Approval(pydantic.BaseModel): @pytest.mark.asyncio async def test_step_replay_skips_llm() -> None: - my_agent = ai.agent(model=MOCK_MODEL) - - @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> ai.StreamResult: - return await ai.stream_step(agent.model, msgs) + my_agent = ai.agent() + # First run: LLM is called. + provider1 = ai.EventLogProvider() llm1 = mock_llm([[text_msg("Hi there!")]]) - result1 = my_agent.run(ai.make_messages(system="test", user="hello")) - [msg async for msg in result1] + async for _msg in my_agent.run( + MOCK_MODEL, + ai.make_messages(system="test", user="hello"), + durability=provider1, + ): + pass assert llm1.call_count == 1 + cp = provider1.checkpoint() - cp = result1.checkpoint + # Replay: LLM is NOT called. llm2 = mock_llm([]) - result2 = my_agent.run(ai.make_messages(system="test", user="hello"), checkpoint=cp) - [msg async for msg in result2] + async for _msg in my_agent.run( + MOCK_MODEL, + ai.make_messages(system="test", user="hello"), + checkpoint=cp, + ): + pass assert llm2.call_count == 0 @@ -52,32 +56,35 @@ async def counting_tool(x: int) -> int: execution_count += 1 return x + 1 - my_agent = ai.agent(model=MOCK_MODEL, tools=[counting_tool]) - - @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> ai.StreamResult: - result = await ai.stream_step(agent.model, msgs, agent.tools) - if result.tool_calls: - await asyncio.gather( - *( - ai.execute_tool(tc, message=result.last_message) - for tc in result.tool_calls - ) - ) - return result + my_agent = ai.agent(tools=[counting_tool]) - mock_llm([[tool_call_msg(tc_id="tc-1", name="counting_tool", args='{"x": 5}')]]) - result1 = my_agent.run(ai.make_messages(system="t", user="go")) - [msg async for msg in result1] + # First run: tool should execute. + mock_llm( + [ + [tool_call_msg(tc_id="tc-1", name="counting_tool", args='{"x": 5}')], + [text_msg("Done", id="msg-2")], + ] + ) + provider1 = ai.EventLogProvider() + async for _msg in my_agent.run( + MOCK_MODEL, + ai.make_messages(system="t", user="go"), + durability=provider1, + ): + pass assert execution_count == 1 - assert result1.checkpoint.tools[0].result == 6 + cp = provider1.checkpoint() + assert cp.tools[0].result == 6 + # Replay: tool should NOT execute. execution_count = 0 mock_llm([]) - result2 = my_agent.run( - ai.make_messages(system="t", user="go"), checkpoint=result1.checkpoint - ) - [msg async for msg in result2] + async for _msg in my_agent.run( + MOCK_MODEL, + ai.make_messages(system="t", user="go"), + checkpoint=cp, + ): + pass assert execution_count == 0 @@ -86,99 +93,65 @@ async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> ai.StreamResult: @pytest.mark.asyncio async def test_hook_cancellation_pending() -> None: - my_agent = ai.agent(model=MOCK_MODEL) + my_agent = ai.agent() @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> Any: - await ai.stream_step(agent.model, msgs) - return await Approval.create("my_approval", metadata={"tool": "test"}) # type: ignore[attr-defined] + async def custom(context: ai.Context) -> AsyncGenerator[ai.Message]: + async for msg in await ai.models.stream(context.model, context.messages): + yield msg + await ai.hook( + "my_approval", + payload=Approval, + metadata={"tool": "test"}, + interrupt_loop=True, + ) mock_llm([[text_msg("OK")]]) - result = my_agent.run(ai.make_messages(system="t", user="go")) - msgs = [msg async for msg in result] - assert "my_approval" in result.pending_hooks + msgs: list[ai.Message] = [] + async for msg in my_agent.run(MOCK_MODEL, ai.make_messages(system="t", user="go")): + msgs.append(msg) + hook_msgs = [m for m in msgs if any(isinstance(p, ai.HookPart) for p in m.parts)] + assert len(hook_msgs) >= 1 assert hook_msgs[0].parts[0].status == "pending" # type: ignore[union-attr] @pytest.mark.asyncio async def test_hook_resolution_on_reentry() -> None: - my_agent = ai.agent(model=MOCK_MODEL) + my_agent = ai.agent() @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> Any: - await ai.stream_step(agent.model, msgs) - return await Approval.create("my_approval") # type: ignore[attr-defined] - - resp = [text_msg("OK")] - mock_llm([resp]) - result1 = my_agent.run(ai.make_messages(system="t", user="go")) - [msg async for msg in result1] - cp = result1.checkpoint - - Approval.resolve("my_approval", {"granted": True}) # type: ignore[attr-defined] - mock_llm([]) - result2 = my_agent.run(ai.make_messages(system="t", user="go"), checkpoint=cp) - [msg async for msg in result2] - assert len(result2.pending_hooks) == 0 - assert result2.checkpoint.hooks[-1].label == "my_approval" - - -@pytest.mark.asyncio -async def test_parallel_hooks_all_collected() -> None: - my_agent = ai.agent(model=MOCK_MODEL) - - @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: - await ai.stream_step(agent.model, msgs) - - async def a() -> Any: - return await Approval.create("hook_a") # type: ignore[attr-defined] - - async def b() -> Any: - return await Approval.create("hook_b") # type: ignore[attr-defined] - - async with asyncio.TaskGroup() as tg: - tg.create_task(a()) - tg.create_task(b()) + async def custom(context: ai.Context) -> AsyncGenerator[ai.Message]: + async for msg in await ai.models.stream(context.model, context.messages): + yield msg + await ai.hook( + "my_approval", + payload=Approval, + interrupt_loop=True, + ) mock_llm([[text_msg("OK")]]) - result = my_agent.run(ai.make_messages(system="t", user="go")) - [msg async for msg in result] - assert {"hook_a", "hook_b"} <= set(result.pending_hooks) - - -@pytest.mark.asyncio -async def test_parallel_hooks_resolve_on_reentry() -> None: - my_agent = ai.agent(model=MOCK_MODEL) - - @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> Any: - await ai.stream_step(agent.model, msgs) - - async def a() -> Any: - return await Approval.create("hook_a") # type: ignore[attr-defined] - - async def b() -> Any: - return await Approval.create("hook_b") # type: ignore[attr-defined] - - async with asyncio.TaskGroup() as tg: - ta = tg.create_task(a()) - tb = tg.create_task(b()) - return ta.result(), tb.result() - - resp = [text_msg("OK")] - mock_llm([resp]) - result1 = my_agent.run(ai.make_messages(system="t", user="go")) - [msg async for msg in result1] - cp = result1.checkpoint - - Approval.resolve("hook_a", {"granted": True}) # type: ignore[attr-defined] - Approval.resolve("hook_b", {"granted": False}) # type: ignore[attr-defined] + provider1 = ai.EventLogProvider() + async for _msg in my_agent.run( + MOCK_MODEL, + ai.make_messages(system="t", user="go"), + durability=provider1, + ): + pass + cp = provider1.checkpoint() + + # Pre-register resolution, then replay. + ai.resolve_hook("my_approval", {"granted": True}) mock_llm([]) - result2 = my_agent.run(ai.make_messages(system="t", user="go"), checkpoint=cp) - [msg async for msg in result2] - assert len(result2.pending_hooks) == 0 + provider2 = ai.EventLogProvider(cp) + async for _msg in my_agent.run( + MOCK_MODEL, + ai.make_messages(system="t", user="go"), + durability=provider2, + ): + pass + cp2 = provider2.checkpoint() + assert any(h.label == "my_approval" for h in cp2.hooks) # -- Serialization --------------------------------------------------------- @@ -189,16 +162,14 @@ def test_checkpoint_serialization_roundtrip() -> None: steps=[ StepEvent( index=0, - messages=[ - ai.Message( - id="m1", - role="assistant", - parts=[ai.TextPart(text="hi")], - ) - ], + message=ai.Message( + id="m1", + role="assistant", + parts=[ai.TextPart(text="hi")], + ), ) ], - tools=[ToolEvent(tool_call_id="tc-1", result=42)], + tools=[ToolEvent(tool_call_id="tc-1", tool_name="test", result=42)], hooks=[HookEvent(label="h1", resolution={"granted": True})], ) cp2 = Checkpoint.model_validate(cp.model_dump()) diff --git a/tests/agents/test_hooks.py b/tests/agents/test_hooks.py index 65bfd15b..58bba9dd 100644 --- a/tests/agents/test_hooks.py +++ b/tests/agents/test_hooks.py @@ -1,7 +1,7 @@ """Hooks: live resolution, cancellation, pre-registration, schema validation.""" import asyncio -from typing import Any, ClassVar +from collections.abc import AsyncGenerator import pydantic import pytest @@ -11,88 +11,75 @@ from ..conftest import MOCK_MODEL, mock_llm, text_msg -@ai.hook class Confirmation(pydantic.BaseModel): approved: bool reason: str = "" -@ai.hook -class CancellingConfirmation(pydantic.BaseModel): - cancels_future: ClassVar[bool] = True - approved: bool - reason: str = "" - - -# -- Hook.resolve() with live future (long-running mode) ------------------- +# -- resolve_hook() with live future (long-running mode) ------------------- @pytest.mark.asyncio async def test_resolve_live_future() -> None: - """In long-running mode, Hook.resolve() unblocks the awaiting coroutine.""" - resolved_value = None - my_agent = ai.agent(model=MOCK_MODEL) + """In long-running mode, resolve_hook() unblocks the awaiting coroutine.""" + resolved_value: Confirmation | None = None + my_agent = ai.agent() @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: + async def custom(context: ai.Context) -> AsyncGenerator[ai.Message]: nonlocal resolved_value - await ai.stream_step(agent.model, msgs) - result = await Confirmation.create("confirm_1") # type: ignore[attr-defined] + async for msg in await ai.models.stream(context.model, context.messages): + yield msg + result = await ai.hook("confirm_1", payload=Confirmation) resolved_value = result mock_llm([[text_msg("OK")]]) - # Confirmation.cancels_future=False -> long-running mode - run_result = my_agent.run(ai.make_messages(user="go")) - collected = [] - async for msg in run_result: - collected.append(msg) - # When we see the pending hook message, resolve it + async for msg in my_agent.run(MOCK_MODEL, ai.make_messages(user="go")): + # When we see the pending hook message, resolve it. if any(isinstance(p, ai.HookPart) and p.status == "pending" for p in msg.parts): - Confirmation.resolve( # type: ignore[attr-defined] - "confirm_1", {"approved": True, "reason": "looks good"} - ) + ai.resolve_hook("confirm_1", {"approved": True, "reason": "looks good"}) assert resolved_value is not None assert resolved_value.approved is True assert resolved_value.reason == "looks good" -# -- Hook.cancel() -------------------------------------------------------- +# -- cancel_hook() -------------------------------------------------------- @pytest.mark.asyncio async def test_cancel_live_hook() -> None: - """Hook.cancel() cancels the future, causing CancelledError in graph.""" + """cancel_hook() cancels the future, causing CancelledError in graph.""" was_cancelled = False - my_agent = ai.agent(model=MOCK_MODEL) + my_agent = ai.agent() @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: + async def custom(context: ai.Context) -> AsyncGenerator[ai.Message]: nonlocal was_cancelled - await ai.stream_step(agent.model, msgs) + async for msg in await ai.models.stream(context.model, context.messages): + yield msg try: - await Confirmation.create("cancel_me") # type: ignore[attr-defined] + await ai.hook("cancel_me", payload=Confirmation) except asyncio.CancelledError: was_cancelled = True mock_llm([[text_msg("OK")]]) - run_result = my_agent.run(ai.make_messages(user="go")) - async for msg in run_result: + async for msg in my_agent.run(MOCK_MODEL, ai.make_messages(user="go")): if any(isinstance(p, ai.HookPart) and p.status == "pending" for p in msg.parts): - await Confirmation.cancel("cancel_me", reason="denied") # type: ignore[attr-defined] + await ai.cancel_hook("cancel_me", reason="denied") assert was_cancelled -# -- Hook.cancel() on non-existent label raises ---------------------------- +# -- cancel_hook() on non-existent label raises ---------------------------- @pytest.mark.asyncio async def test_cancel_nonexistent_raises() -> None: with pytest.raises(ValueError, match="No pending hook"): - await Confirmation.cancel("does_not_exist_xyz") # type: ignore[attr-defined] + await ai.cancel_hook("does_not_exist_xyz") # -- Pre-registration (serverless re-entry) -------------------------------- @@ -100,36 +87,44 @@ async def test_cancel_nonexistent_raises() -> None: @pytest.mark.asyncio async def test_pre_registered_resolution_consumed() -> None: - """Pre-registered resolution is consumed by Hook.create() without suspending.""" - my_agent = ai.agent(model=MOCK_MODEL) + """Pre-registered resolution is consumed by hook() without suspending.""" + my_agent = ai.agent() @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> Any: - await ai.stream_step(agent.model, msgs) - result = await Confirmation.create("pre_reg_1") # type: ignore[attr-defined] - return result + async def custom(context: ai.Context) -> AsyncGenerator[ai.Message]: + async for msg in await ai.models.stream(context.model, context.messages): + yield msg + await ai.hook("pre_reg_1", payload=Confirmation) - # Pre-register BEFORE run - Confirmation.resolve("pre_reg_1", {"approved": True}) # type: ignore[attr-defined] + # Pre-register BEFORE run. + ai.resolve_hook("pre_reg_1", {"approved": True}) mock_llm([[text_msg("OK")]]) - run_result = my_agent.run(ai.make_messages(user="go")) - [m async for m in run_result] + provider = ai.EventLogProvider() + async for _msg in my_agent.run( + MOCK_MODEL, + ai.make_messages(user="go"), + durability=provider, + ): + pass - # Should have completed with no pending hooks - assert len(run_result.pending_hooks) == 0 - # Hook event should be in checkpoint - assert any(h.label == "pre_reg_1" for h in run_result.checkpoint.hooks) + # Hook event should be recorded in checkpoint. + cp = provider.checkpoint() + assert any(h.label == "pre_reg_1" for h in cp.hooks) # -- Schema validation on resolve ----------------------------------------- def test_resolve_validates_schema() -> None: - """resolve() with invalid data raises from pydantic validation.""" - # 'approved' is required bool, passing string should raise + """resolve_hook() with invalid data raises from pydantic validation.""" + # 'approved' is required bool, passing string should raise. with pytest.raises(pydantic.ValidationError): - Confirmation.resolve("schema_test", {"approved": "not_a_bool"}) # type: ignore[attr-defined] + ai.resolve_hook( + "schema_test", + {"approved": "not_a_bool"}, + payload=Confirmation, + ) # -- Resolved hook emits message ------------------------------------------- @@ -138,21 +133,21 @@ def test_resolve_validates_schema() -> None: @pytest.mark.asyncio async def test_resolved_hook_emits_message() -> None: """After resolution, a 'resolved' HookPart message is emitted.""" - my_agent = ai.agent(model=MOCK_MODEL) + my_agent = ai.agent() @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: - await ai.stream_step(agent.model, msgs) - await Confirmation.create("emit_test") # type: ignore[attr-defined] + async def custom(context: ai.Context) -> AsyncGenerator[ai.Message]: + async for msg in await ai.models.stream(context.model, context.messages): + yield msg + await ai.hook("emit_test", payload=Confirmation) mock_llm([[text_msg("OK")]]) - run_result = my_agent.run(ai.make_messages(user="go")) - msgs = [] - async for msg in run_result: + msgs: list[ai.Message] = [] + async for msg in my_agent.run(MOCK_MODEL, ai.make_messages(user="go")): msgs.append(msg) if any(isinstance(p, ai.HookPart) and p.status == "pending" for p in msg.parts): - Confirmation.resolve("emit_test", {"approved": False}) # type: ignore[attr-defined] + ai.resolve_hook("emit_test", {"approved": False}) hook_msgs = [ m @@ -160,7 +155,7 @@ async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: if any(isinstance(p, ai.HookPart) and p.status == "resolved" for p in m.parts) ] assert len(hook_msgs) == 1 - assert hook_msgs[0].parts[0].resolution == {"approved": False, "reason": ""} # type: ignore[union-attr] + assert hook_msgs[0].parts[0].resolution == {"approved": False} # type: ignore[union-attr] # -- Hook metadata surfaces in pending message ----------------------------- @@ -168,18 +163,24 @@ async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: @pytest.mark.asyncio async def test_hook_metadata_in_pending() -> None: - my_agent = ai.agent(model=MOCK_MODEL) + my_agent = ai.agent() @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: - await ai.stream_step(agent.model, msgs) - await CancellingConfirmation.create( # type: ignore[attr-defined] - "meta_test", metadata={"tool": "rm -rf", "path": "/"} + async def custom(context: ai.Context) -> AsyncGenerator[ai.Message]: + async for msg in await ai.models.stream(context.model, context.messages): + yield msg + await ai.hook( + "meta_test", + payload=Confirmation, + metadata={"tool": "rm -rf", "path": "/"}, + interrupt_loop=True, ) mock_llm([[text_msg("OK")]]) - run_result = my_agent.run(ai.make_messages(user="go")) - [m async for m in run_result] + msgs: list[ai.Message] = [] + async for msg in my_agent.run(MOCK_MODEL, ai.make_messages(user="go")): + msgs.append(msg) - info = run_result.pending_hooks["meta_test"] - assert info.metadata == {"tool": "rm -rf", "path": "/"} + hook_msgs = [m for m in msgs if any(isinstance(p, ai.HookPart) for p in m.parts)] + assert len(hook_msgs) >= 1 + assert hook_msgs[0].parts[0].metadata == {"tool": "rm -rf", "path": "/"} # type: ignore[union-attr] diff --git a/tests/agents/test_runtime.py b/tests/agents/test_runtime.py index 4027d962..2efe50a0 100644 --- a/tests/agents/test_runtime.py +++ b/tests/agents/test_runtime.py @@ -1,11 +1,8 @@ -"""Agent default loop, execute_tool, multi-turn, Runtime injection.""" - -import asyncio +"""Agent default loop, tool execution, multi-turn.""" import pytest import vercel_ai_sdk as ai -from vercel_ai_sdk.agents.runtime import Runtime from vercel_ai_sdk.types import messages from ..conftest import MOCK_MODEL, mock_llm, text_msg, tool_call_msg @@ -31,11 +28,12 @@ async def concat(a: str, b: str) -> str: @pytest.mark.asyncio async def test_agent_text_only() -> None: """Agent default loop with no tool calls returns after one LLM call.""" - my_agent = ai.agent(model=MOCK_MODEL, tools=[double]) + my_agent = ai.agent(tools=[double]) llm = mock_llm([[text_msg("Hello!")]]) - result = my_agent.run(ai.make_messages(user="Hi")) - msgs = [m async for m in result] + msgs: list[ai.Message] = [] + async for m in my_agent.run(MOCK_MODEL, ai.make_messages(user="Hi")): + msgs.append(m) assert llm.call_count == 1 assert any(m.text == "Hello!" for m in msgs) @@ -46,16 +44,16 @@ async def test_agent_text_only() -> None: @pytest.mark.asyncio async def test_agent_tool_then_text() -> None: """Agent default loop calls tool, feeds result back, gets final text.""" - my_agent = ai.agent(model=MOCK_MODEL, tools=[double]) + my_agent = ai.agent(tools=[double]) call1 = [tool_call_msg(tc_id="tc-1", name="double", args='{"x": 5}')] call2 = [text_msg("The answer is 10.")] llm = mock_llm([call1, call2]) - result = my_agent.run(ai.make_messages(user="Double 5")) - msgs = [m async for m in result] + msgs: list[ai.Message] = [] + async for m in my_agent.run(MOCK_MODEL, ai.make_messages(user="Double 5")): + msgs.append(m) assert llm.call_count == 2 - # Tool should have been executed: 5 * 2 = 10 tool_results = [m for m in msgs if m.role == "tool" and m.tool_results] assert len(tool_results) >= 1 assert tool_results[0].tool_results[0].result == 10 @@ -67,7 +65,7 @@ async def test_agent_tool_then_text() -> None: @pytest.mark.asyncio async def test_agent_parallel_tools() -> None: """LLM returns two tool calls in one message; both execute.""" - my_agent = ai.agent(model=MOCK_MODEL, tools=[double]) + my_agent = ai.agent(tools=[double]) two_tools = messages.Message( id="msg-1", @@ -90,10 +88,10 @@ async def test_agent_parallel_tools() -> None: call2 = [text_msg("6 and 14", id="msg-2")] llm = mock_llm([[two_tools], call2]) - result = my_agent.run(ai.make_messages(user="Double 3 and 7")) - msgs = [m async for m in result] + msgs: list[ai.Message] = [] + async for m in my_agent.run(MOCK_MODEL, ai.make_messages(user="Double 3 and 7")): + msgs.append(m) assert llm.call_count == 2 - # Both tools should have results tool_result_msgs = [m for m in msgs if m.role == "tool" and m.tool_results] assert len(tool_result_msgs) >= 1 @@ -104,7 +102,7 @@ async def test_agent_parallel_tools() -> None: @pytest.mark.asyncio async def test_agent_multi_turn() -> None: """LLM calls a tool, then calls another tool, then returns text.""" - my_agent = ai.agent(model=MOCK_MODEL, tools=[double, concat]) + my_agent = ai.agent(tools=[double, concat]) turn1 = [ tool_call_msg(tc_id="tc-1", name="concat", args='{"a": "hello", "b": " world"}') @@ -113,114 +111,9 @@ async def test_agent_multi_turn() -> None: turn3 = [text_msg("Done: hello world, 6", id="msg-3")] llm = mock_llm([turn1, turn2, turn3]) - result = my_agent.run(ai.make_messages(user="Concat then double")) - [m async for m in result] + msgs: list[ai.Message] = [] + async for m in my_agent.run( + MOCK_MODEL, ai.make_messages(user="Concat then double") + ): + msgs.append(m) assert llm.call_count == 3 - - -# -- execute_tool: missing tool raises ------------------------------------ - - -@pytest.mark.asyncio -async def test_execute_tool_missing_raises() -> None: - """execute_tool with unknown tool name raises ValueError. - - Wrapped in ExceptionGroup by TaskGroup. - """ - tc = messages.ToolCallPart( - tool_call_id="tc-1", tool_name="nonexistent_tool_zzz", tool_args="{}" - ) - my_agent = ai.agent(model=MOCK_MODEL, tools=[]) - - @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: - await ai.execute_tool(tc) - - mock_llm([]) - result = my_agent.run(ai.make_messages(user="go")) - with pytest.raises(ExceptionGroup) as exc_info: - [m async for m in result] - assert any(isinstance(e, ValueError) for e in exc_info.value.exceptions) - - -# -- execute_tool: Runtime injection --------------------------------------- - - -@pytest.mark.asyncio -async def test_execute_tool_injects_runtime() -> None: - """Tools with a Runtime parameter get the active runtime injected.""" - received_rt = None - - @ai.tool - async def introspect(query: str, rt: Runtime) -> str: - """Tool that inspects runtime.""" - nonlocal received_rt - received_rt = rt - return "ok" - - my_agent = ai.agent(model=MOCK_MODEL, tools=[introspect]) - - @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: - result = await ai.stream_step(agent.model, msgs, agent.tools) - if result.tool_calls: - await asyncio.gather( - *( - ai.execute_tool(tc, message=result.last_message) - for tc in result.tool_calls - ) - ) - - call = [tool_call_msg(tc_id="tc-1", name="introspect", args='{"query": "test"}')] - mock_llm([call]) - result = my_agent.run(ai.make_messages(user="go")) - [m async for m in result] - assert received_rt is not None - assert isinstance(received_rt, Runtime) - - -# -- execute_tool: returns updated ToolPart -------------------------------- - - -@pytest.mark.asyncio -async def test_execute_tool_returns_result_part() -> None: - """execute_tool returns a ToolResultPart; the original ToolCallPart is unchanged.""" - my_agent = ai.agent(model=MOCK_MODEL, tools=[double]) - - @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> None: - result = await ai.stream_step(agent.model, msgs, agent.tools) - if result.tool_calls: - msg = result.last_message - assert msg is not None - for tc in result.tool_calls: - result_part = await ai.execute_tool(tc, message=msg) - # Returned part is a ToolResultPart with the result - assert not result_part.is_error - assert result_part.result == 10 - # Original message's tool calls are unchanged (immutable) - assert msg.tool_calls[0].tool_name == "double" - - call = [tool_call_msg(tc_id="tc-1", name="double", args='{"x": 5}')] - mock_llm([call]) - result = my_agent.run(ai.make_messages(user="go")) - [m async for m in result] - - -# -- Checkpoint records tools from Agent default loop ---------------------- - - -@pytest.mark.asyncio -async def test_agent_checkpoint_records_tools() -> None: - """Agent default loop's tool executions are recorded in the checkpoint.""" - my_agent = ai.agent(model=MOCK_MODEL, tools=[double]) - - call1 = [tool_call_msg(tc_id="tc-1", name="double", args='{"x": 4}')] - call2 = [text_msg("8", id="msg-2")] - mock_llm([call1, call2]) - - result = my_agent.run(ai.make_messages(user="Double 4")) - [m async for m in result] - - cp = result.checkpoint - assert any(t.tool_call_id == "tc-1" and t.result == 8 for t in cp.tools) diff --git a/tests/agents/test_streams.py b/tests/agents/test_streams.py deleted file mode 100644 index 4bf546ef..00000000 --- a/tests/agents/test_streams.py +++ /dev/null @@ -1,113 +0,0 @@ -"""@stream decorator: context requirement, replay, queue submission.""" - -import pydantic -import pytest - -import vercel_ai_sdk as ai -from vercel_ai_sdk.agents.streams import StreamResult -from vercel_ai_sdk.types import messages - -from ..conftest import MOCK_MODEL, mock_llm, text_msg - - -class _Weather(pydantic.BaseModel): - city: str - temperature: float - - -# -- StreamResult properties ----------------------------------------------- - - -def test_stream_result_empty() -> None: - r = StreamResult() - assert r.last_message is None - assert r.tool_calls == [] - assert r.text == "" - - -def test_stream_result_last_message() -> None: - m1 = text_msg("first", id="m1") - m2 = text_msg("second", id="m2") - r = StreamResult(messages=[m1, m2]) - last = r.last_message - assert last is not None - assert last.id == "m2" - assert r.text == "second" - - -def test_stream_result_tool_calls() -> None: - m = messages.Message( - id="m1", - role="assistant", - parts=[ - messages.ToolCallPart( - tool_call_id="tc1", tool_name="t", tool_args="{}", state="done" - ), - messages.ToolCallPart( - tool_call_id="tc2", tool_name="u", tool_args="{}", state="done" - ), - ], - ) - r = StreamResult(messages=[m]) - assert len(r.tool_calls) == 2 - - -# -- @stream requires Runtime context ------------------------------------- - - -@pytest.mark.asyncio -async def test_stream_outside_run_raises() -> None: - """@stream-decorated fn called without ai.run() should raise.""" - mock_llm([[text_msg("hi")]]) - with pytest.raises(ValueError, match="No Runtime context"): - await ai.stream_step( - MOCK_MODEL, - ai.make_messages(user="test"), - ) - - -# -- @stream replays from checkpoint -------------------------------------- - - -@pytest.mark.asyncio -async def test_stream_step_replays_from_checkpoint() -> None: - """stream_step inside Agent.run with a checkpoint replays without calling LLM.""" - - my_agent = ai.agent(model=MOCK_MODEL) - - @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> ai.StreamResult: - return await ai.stream_step(agent.model, msgs) - - # First run - mock_llm([[text_msg("Hi")]]) - r1 = my_agent.run(ai.make_messages(user="hello")) - [msg async for msg in r1] - cp = r1.checkpoint - - # Replay - llm2 = mock_llm([]) - r2 = my_agent.run(ai.make_messages(user="hello"), checkpoint=cp) - [msg async for msg in r2] - assert llm2.call_count == 0 - - -# -- StreamResult.output --------------------------------------------------- - - -def test_stream_result_output_from_last_message() -> None: - """StreamResult.output delegates to the last message's StructuredOutputPart.""" - m = messages.Message( - id="m1", - role="assistant", - parts=[ - messages.TextPart(text="{}", state="done"), - messages.StructuredOutputPart( - data={"city": "SF", "temperature": 62.0}, - output_type_name=f"{_Weather.__module__}.{_Weather.__qualname__}", - ), - ], - ) - r = StreamResult(messages=[text_msg("streaming..."), m]) - assert r.output is not None - assert r.output.city == "SF" diff --git a/tests/agents/test_tools.py b/tests/agents/test_tools.py index 33a11672..70c75dde 100644 --- a/tests/agents/test_tools.py +++ b/tests/agents/test_tools.py @@ -1,10 +1,8 @@ -"""@tool decorator: schema extraction, registry, Runtime parameter handling.""" +"""@tool decorator: schema extraction, execution, ToolCall.""" import pytest import vercel_ai_sdk as ai -from vercel_ai_sdk.agents.runtime import Runtime -from vercel_ai_sdk.agents.tools import get_tool # -- Schema extraction from type hints ------------------------------------ @@ -31,7 +29,6 @@ async def search(query: str, limit: int | None = None) -> str: assert "query" in search.param_schema.get("required", []) assert "limit" not in search.param_schema.get("required", []) - # limit should still appear in properties assert "limit" in search.param_schema["properties"] @@ -41,8 +38,8 @@ async def fetch(url: str, timeout: int = 30) -> str: """Fetch URL.""" return url - assert "url" in search_required(fetch) - assert "timeout" not in search_required(fetch) + assert "url" in _required(fetch) + assert "timeout" not in _required(fetch) def test_complex_type_schema() -> None: @@ -56,55 +53,67 @@ async def send(recipients: list[str], urgent: bool = False) -> str: assert props["recipients"]["items"]["type"] == "string" -# -- Runtime parameter skipping ------------------------------------------- +# -- Execution (Tool.__call__) -------------------------------------------- -def test_runtime_param_excluded_from_schema() -> None: +@pytest.mark.asyncio +async def test_tool_call_with_json_args() -> None: @ai.tool - async def needs_runtime(query: str, rt: Runtime) -> str: - """Tool that needs runtime.""" - return query + async def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b - props = needs_runtime.param_schema["properties"] - assert "rt" not in props - assert "query" in props - assert set(needs_runtime.param_schema.get("required", [])) == {"query"} + result = await add('{"a": 1, "b": 2}') + assert result == 3 -# -- Registry ------------------------------------------------------------- +# -- ToolCall binds a ToolCallPart to a Tool and returns ToolResultPart ---- -def test_tool_registered_on_decoration() -> None: +@pytest.mark.asyncio +async def test_tool_call_returns_result_part() -> None: @ai.tool - async def unique_tool_abc() -> str: - """Unique.""" - return "ok" - - assert get_tool("unique_tool_abc") is unique_tool_abc + async def double(x: int) -> int: + """Double a number.""" + return x * 2 + part = ai.ToolCallPart( + tool_call_id="tc-1", + tool_name="double", + tool_args='{"x": 5}', + ) + tc = ai.ToolCall(part=part, tool=double) + result = await tc() -def test_get_tool_returns_none_for_missing() -> None: - assert get_tool("nonexistent_tool_xyz") is None - - -# -- Execution ------------------------------------------------------------ + assert result.tool_call_id == "tc-1" + assert result.tool_name == "double" + assert result.result == 10 + assert not result.is_error @pytest.mark.asyncio -async def test_tool_fn_is_callable() -> None: +async def test_tool_call_catches_errors() -> None: @ai.tool - async def add(a: int, b: int) -> int: - """Add two numbers.""" - return a + b + async def fail(x: int) -> int: + """Always fails.""" + raise ValueError("boom") - result = await add(a=1, b=2) - assert result == 3 + part = ai.ToolCallPart( + tool_call_id="tc-err", + tool_name="fail", + tool_args='{"x": 1}', + ) + tc = ai.ToolCall(part=part, tool=fail) + result = await tc() + + assert result.is_error + assert "boom" in str(result.result) # -- Helpers --------------------------------------------------------------- -def search_required(tool: ai.Tool[..., object]) -> list[str]: +def _required(tool: ai.Tool[..., object]) -> list[str]: result = tool.param_schema.get("required", []) assert isinstance(result, list) return result diff --git a/tests/telemetry/test_otel_handler.py b/tests/telemetry/test_otel_handler.py index 88f54f58..40c0fc2b 100644 --- a/tests/telemetry/test_otel_handler.py +++ b/tests/telemetry/test_otel_handler.py @@ -35,11 +35,11 @@ async def double(x: int) -> int: @pytest.mark.asyncio async def test_text_only_spans(spans: InMemorySpanExporter) -> None: """Text-only run produces ai.run > ai.stream span hierarchy.""" - my_agent = ai.agent(model=MOCK_MODEL, tools=[]) + my_agent = ai.agent() mock_llm([[text_msg("Hello!")]]) - result = my_agent.run(ai.make_messages(user="Hi")) - [m async for m in result] + async for _m in my_agent.run(MOCK_MODEL, ai.make_messages(user="Hi")): + pass finished = spans.get_finished_spans() names = [s.name for s in finished] @@ -64,7 +64,7 @@ async def test_text_only_spans(spans: InMemorySpanExporter) -> None: @pytest.mark.asyncio async def test_tool_call_spans(spans: InMemorySpanExporter) -> None: """Tool-calling run produces ai.tool spans with correct attributes.""" - my_agent = ai.agent(model=MOCK_MODEL, tools=[double]) + my_agent = ai.agent(tools=[double]) mock_llm( [ @@ -72,8 +72,8 @@ async def test_tool_call_spans(spans: InMemorySpanExporter) -> None: [text_msg("10")], ] ) - result = my_agent.run(ai.make_messages(user="Double 5")) - [m async for m in result] + async for _m in my_agent.run(MOCK_MODEL, ai.make_messages(user="Double 5")): + pass finished = spans.get_finished_spans() names = [s.name for s in finished] diff --git a/tests/telemetry/test_telemetry.py b/tests/telemetry/test_telemetry.py index 04667f78..640055d4 100644 --- a/tests/telemetry/test_telemetry.py +++ b/tests/telemetry/test_telemetry.py @@ -3,7 +3,7 @@ from __future__ import annotations import dataclasses -from collections.abc import Generator +from collections.abc import AsyncGenerator, Generator from typing import Any import pytest @@ -55,11 +55,11 @@ async def double(x: int) -> int: @pytest.mark.asyncio async def test_text_only_run_events(handler: RecordingHandler) -> None: """Simplest run emits RunStart, StepStart, StepFinish, RunFinish.""" - my_agent = ai.agent(model=MOCK_MODEL, tools=[]) + my_agent = ai.agent() mock_llm([[text_msg("Hello!")]]) - result = my_agent.run(ai.make_messages(user="Hi")) - [m async for m in result] + async for _m in my_agent.run(MOCK_MODEL, ai.make_messages(user="Hi")): + pass types = [type(e).__name__ for e in handler.events] assert types == [ @@ -77,7 +77,7 @@ async def test_text_only_run_events(handler: RecordingHandler) -> None: @pytest.mark.asyncio async def test_tool_call_events(handler: RecordingHandler) -> None: """Tool-calling run emits tool events between steps.""" - my_agent = ai.agent(model=MOCK_MODEL, tools=[double]) + my_agent = ai.agent(tools=[double]) mock_llm( [ @@ -85,8 +85,8 @@ async def test_tool_call_events(handler: RecordingHandler) -> None: [text_msg("10")], ] ) - result = my_agent.run(ai.make_messages(user="Double 5")) - [m async for m in result] + async for _m in my_agent.run(MOCK_MODEL, ai.make_messages(user="Double 5")): + pass types = [type(e).__name__ for e in handler.events] assert types == [ @@ -125,11 +125,11 @@ def handle(self, event: TelemetryEvent) -> None: ai.telemetry.enable(Capture()) try: - my_agent = ai.agent(model=MOCK_MODEL, tools=[]) + my_agent = ai.agent() mock_llm([[text_msg("Hello!")]]) - result = my_agent.run(ai.make_messages(user="Hi")) - [m async for m in result] + async for _m in my_agent.run(MOCK_MODEL, ai.make_messages(user="Hi")): + pass assert len(captured) == 16 finally: ai.telemetry.disable() @@ -144,19 +144,19 @@ async def test_disable_reverts_to_noop() -> None: handler = RecordingHandler() ai.telemetry.enable(handler) - my_agent = ai.agent(model=MOCK_MODEL, tools=[]) + my_agent = ai.agent() mock_llm([[text_msg("Hello!")]]) - result = my_agent.run(ai.make_messages(user="Hi")) - [m async for m in result] + async for _m in my_agent.run(MOCK_MODEL, ai.make_messages(user="Hi")): + pass assert len(handler.of_type(RunStartEvent)) == 1 ai.telemetry.disable() handler.events.clear() mock_llm([[text_msg("Hello!")]]) - result = my_agent.run(ai.make_messages(user="Hi")) - [m async for m in result] + async for _m in my_agent.run(MOCK_MODEL, ai.make_messages(user="Hi")): + pass assert len(handler.events) == 0 @@ -171,16 +171,17 @@ async def test_user_emitted_custom_event(handler: RecordingHandler) -> None: class CustomEvent(TelemetryEvent): message: str - my_agent = ai.agent(model=MOCK_MODEL, tools=[]) + my_agent = ai.agent() @my_agent.loop - async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> ai.StreamResult: + async def custom(context: ai.Context) -> AsyncGenerator[ai.Message]: ai.telemetry.handle(CustomEvent(message="hello")) - return await ai.stream_step(agent.model, msgs) + async for msg in await ai.models.stream(context.model, context.messages): + yield msg mock_llm([[text_msg("Hello!")]]) - result = my_agent.run(ai.make_messages(user="Hi")) - [m async for m in result] + async for _m in my_agent.run(MOCK_MODEL, ai.make_messages(user="Hi")): + pass custom_events = [e for e in handler.events if isinstance(e, CustomEvent)] assert len(custom_events) == 1 @@ -193,16 +194,17 @@ async def custom(agent: ai.Agent, msgs: list[ai.Message]) -> ai.StreamResult: @pytest.mark.asyncio async def test_run_error_in_finish_event(handler: RecordingHandler) -> None: """RunFinishEvent captures the error when the loop function raises.""" - my_agent = ai.agent(model=MOCK_MODEL, tools=[]) + my_agent = ai.agent() @my_agent.loop - async def failing(agent: ai.Agent, msgs: list[ai.Message]) -> None: + async def failing(context: ai.Context) -> AsyncGenerator[ai.Message]: raise ValueError("boom") + yield # make it a generator mock_llm([]) - result = my_agent.run(ai.make_messages(user="Hi")) with pytest.raises(ExceptionGroup): - [m async for m in result] + async for _m in my_agent.run(MOCK_MODEL, ai.make_messages(user="Hi")): + pass run_finish = handler.of_type(RunFinishEvent)[0] assert run_finish.error is not None