From cd560b6f059fa25cca7b97d97e137f712bc99964 Mon Sep 17 00:00:00 2001 From: Andrey Buzin Date: Thu, 12 Feb 2026 19:26:21 -0800 Subject: [PATCH 1/5] Make a first approximation of a temporal durable agent example --- examples/temporal-durable/.gitignore | 10 + examples/temporal-durable/.python-version | 1 + examples/temporal-durable/README.md | 92 +++ examples/temporal-durable/activities.py | 118 +++ examples/temporal-durable/durable.py | 122 +++ examples/temporal-durable/main.py | 79 ++ examples/temporal-durable/pyproject.toml | 12 + examples/temporal-durable/tools.py | 22 + examples/temporal-durable/uv.lock | 900 ++++++++++++++++++++++ examples/temporal-durable/workflow.py | 92 +++ 10 files changed, 1448 insertions(+) create mode 100644 examples/temporal-durable/.gitignore create mode 100644 examples/temporal-durable/.python-version create mode 100644 examples/temporal-durable/README.md create mode 100644 examples/temporal-durable/activities.py create mode 100644 examples/temporal-durable/durable.py create mode 100644 examples/temporal-durable/main.py create mode 100644 examples/temporal-durable/pyproject.toml create mode 100644 examples/temporal-durable/tools.py create mode 100644 examples/temporal-durable/uv.lock create mode 100644 examples/temporal-durable/workflow.py diff --git a/examples/temporal-durable/.gitignore b/examples/temporal-durable/.gitignore new file mode 100644 index 00000000..393fc622 --- /dev/null +++ b/examples/temporal-durable/.gitignore @@ -0,0 +1,10 @@ +# 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 new file mode 100644 index 00000000..e4fba218 --- /dev/null +++ b/examples/temporal-durable/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/examples/temporal-durable/README.md b/examples/temporal-durable/README.md new file mode 100644 index 00000000..2abb3bcc --- /dev/null +++ b/examples/temporal-durable/README.md @@ -0,0 +1,92 @@ +# Durable Agent Execution with Temporal + +Reproduces the `custom_loop.py` sample with Temporal durable execution. +LLM calls and tool calls are Temporal activities; the agent loop is a +Temporal workflow. The whole thing survives crashes and restarts. + +## How it works + +``` +workflow.py durable.py activities.py +┌──────────┐ ┌───────────────────┐ ┌──────────────────┐ +│ ai.run() │───>│TemporalLanguage │───>│llm_stream_activity│ +│ agent() │ │Model.stream() │ │ (real LLM call) │ +│ │ │ execute_activity()│ │ │ +│ │ ├───────────────────┤ ├──────────────────┤ +│ execute_ │───>│temporal_tool().fn │───>│tool_call_activity │ +│ tool() │ │ execute_activity()│ │ (real tool call) │ +└──────────┘ └───────────────────┘ └──────────────────┘ + WORKFLOW WRAPPERS ACTIVITIES (I/O) + (deterministic) (routing) (non-deterministic) +``` + +- **Workflow** (`workflow.py`): The agent loop runs inside `ai.run()`, unchanged + from the non-durable version. Uses `asyncio.TaskGroup`, `Queue`, `Future` — all + fine inside Temporal's custom event loop. +- **Wrappers** (`durable.py`): `TemporalLanguageModel` and `temporal_tool()` replace + real I/O with `workflow.execute_activity()` calls. On replay, Temporal returns + cached results. +- **Activities** (`activities.py`): Real I/O happens here — LLM API calls and tool + function execution. Activities are retried automatically on failure. + +## Setup + +### 1. Install Temporal CLI + +```bash +brew install temporal # macOS +# or: curl -sSf https://temporal.download/cli.sh | sh +``` + +### 2. Start Temporal dev server + +```bash +temporal server start-dev +``` + +This starts a local server on `localhost:7233` with a UI at `http://localhost:8233`. + +### 3. Install dependencies + +```bash +cd examples/temporal-durable +uv sync +``` + +### 4. Set environment variables + +```bash +export AI_GATEWAY_API_KEY=your-key-here +``` + +### 5. Run + +```bash +uv run python main.py +# or with a custom query: +uv run python main.py "What is the weather in Tokyo?" +``` + +## Architecture notes + +### Streaming limitation + +Temporal activities are request/response — they can't stream. The +`TemporalLanguageModel` buffers the full LLM response inside the activity +and returns it as a single result. The workflow sees a "stream" that +completes in one shot. + +### Tool registry hack + +`temporal_tool()` replaces the global tool registry entry with the +activity-calling wrapper, and stashes the original function for the +activity to use. This is the "dumbest version" — a proper framework +primitive will replace this once the overall shape of durable execution +settles. + +### Sandbox passthrough + +Temporal's workflow sandbox re-imports modules on each replay. We use +`workflow.unsafe.imports_passed_through()` to pass `vercel_ai_sdk` and +our local modules through unchanged, since they don't do non-deterministic +things at import time. diff --git a/examples/temporal-durable/activities.py b/examples/temporal-durable/activities.py new file mode 100644 index 00000000..cc1f5098 --- /dev/null +++ b/examples/temporal-durable/activities.py @@ -0,0 +1,118 @@ +"""Temporal activities — all real I/O lives here. + +Two activities: + 1. llm_stream_activity — calls the real LLM, drains the stream, returns messages + 2. tool_call_activity — calls the real tool function, returns the result + +These run in the activity context (outside the workflow sandbox), so they can +do network I/O, access environment variables, etc. +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from typing import Any + +from temporalio import activity + +import vercel_ai_sdk as ai +from vercel_ai_sdk.anthropic import AnthropicModel + + +# ── Serializable parameter / result types ──────────────────────── + + +@dataclass +class LLMCallParams: + """Input to the LLM activity. All fields are JSON-serializable.""" + + messages: list[dict[str, Any]] # [Message.model_dump(), ...] + tool_schemas: list[dict[str, Any]] # [{name, description, schema}, ...] + + +@dataclass +class LLMCallResult: + """Output from the LLM activity.""" + + messages: list[dict[str, Any]] # final accumulated messages from the stream + + +@dataclass +class ToolCallParams: + """Input to the tool call activity.""" + + tool_name: str + tool_args: dict[str, Any] + + +# ── Stash for original tool functions ──────────────────────────── +# temporal_tool() puts originals here before replacing .fn with an +# activity-calling wrapper. The activity looks them up from here. + +_original_tool_fns: dict[str, Any] = {} + + +def register_original_fn(name: str, fn: Any) -> None: + _original_tool_fns[name] = fn + + +def get_original_fn(name: str) -> Any: + return _original_tool_fns[name] + + +# ── Activities ─────────────────────────────────────────────────── + + +@activity.defn(name="llm_stream") +async def llm_stream_activity(params: LLMCallParams) -> LLMCallResult: + """Call the LLM, drain the full stream, return the final messages. + + The real AnthropicModel is constructed here from environment variables. + The stream is consumed fully — the workflow only sees the final result. + """ + # Hardcoded for the example — in production, model config would be + # passed through or resolved from a registry. + llm = AnthropicModel( + model="anthropic/claude-sonnet-4", + base_url="https://ai-gateway.vercel.sh", + api_key=os.environ.get("AI_GATEWAY_API_KEY"), + ) + + # Reconstruct Message objects from serialized dicts + messages = [ai.Message.model_validate(m) for m in params.messages] + + # Reconstruct Tool objects (schema-only, for the LLM to know what's available) + tools = [ + ai.Tool( + name=t["name"], + description=t["description"], + schema=t["schema"], + fn=lambda: None, # placeholder — not called, just for schema + ) + for t in params.tool_schemas + ] + + # Drain the stream, keep all emitted messages + result_messages: list[ai.Message] = [] + async for msg in llm.stream(messages=messages, tools=tools or None): + result_messages.append(msg) + + # Only return the final message (the one with is_done=True and all parts settled) + # Earlier messages are intermediate streaming states of the same message. + final = result_messages[-1] if result_messages else None + if final is None: + return LLMCallResult(messages=[]) + + return LLMCallResult(messages=[final.model_dump()]) + + +@activity.defn(name="tool_call") +async def tool_call_activity(params: ToolCallParams) -> Any: + """Execute a tool function by name with the given arguments. + + Looks up the *original* function (not the temporal wrapper) from the + stash populated by temporal_tool(). + """ + fn = get_original_fn(params.tool_name) + return await fn(**params.tool_args) diff --git a/examples/temporal-durable/durable.py b/examples/temporal-durable/durable.py new file mode 100644 index 00000000..edeaa4ec --- /dev/null +++ b/examples/temporal-durable/durable.py @@ -0,0 +1,122 @@ +"""Durable execution primitives — thin wrappers that route I/O through Temporal activities. + +Two primitives: + 1. TemporalLanguageModel — LanguageModel whose stream() calls an activity + 2. temporal_tool() — wraps a Tool so its fn calls an activity + +The agent code stays identical to the non-durable version. These wrappers +are the only things that know about Temporal. +""" + +from __future__ import annotations + +from collections.abc import AsyncGenerator +from datetime import timedelta +from typing import Any, override + +from temporalio import workflow +from temporalio.common import RetryPolicy + +import vercel_ai_sdk as ai +from vercel_ai_sdk.core.tools import _tool_registry + +from activities import ( + LLMCallParams, + LLMCallResult, + ToolCallParams, + llm_stream_activity, + register_original_fn, + tool_call_activity, +) + + +# ── TemporalLanguageModel ──────────────────────────────────────── + + +class TemporalLanguageModel(ai.LanguageModel): + """LanguageModel that delegates to a Temporal activity for each LLM call. + + Instead of hitting the real API, stream() calls workflow.execute_activity() + which runs the real LLM call inside an activity (with retry, timeout, etc.). + + The activity drains the full stream and returns the final messages. We + yield them as-if they arrived all at once — the "buffering wrapper" that + bridges streaming to Temporal's request/response model. + """ + + @override + async def stream( + self, + messages: list[ai.Message], + tools: list[ai.Tool] | None = None, + ) -> AsyncGenerator[ai.Message, None]: + # Serialize for the activity boundary + tool_schemas = [ + {"name": t.name, "description": t.description, "schema": t.schema} + for t in (tools or []) + ] + + params = LLMCallParams( + messages=[m.model_dump() for m in messages], + tool_schemas=tool_schemas, + ) + + # Call the activity — this is the durable boundary. + # On replay, Temporal returns the cached result without re-executing. + result: LLMCallResult = await workflow.execute_activity( + llm_stream_activity, + params, + start_to_close_timeout=timedelta(minutes=5), + retry_policy=RetryPolicy(maximum_attempts=3), + ) + + # Yield the final message(s) — the @ai.stream decorator and + # Runtime pump see this as a (very fast) stream that completes + # in one shot. + for msg_dict in result.messages: + yield ai.Message.model_validate(msg_dict) + + +# ── temporal_tool ──────────────────────────────────────────────── + + +def temporal_tool(t: ai.Tool) -> ai.Tool: + """Wrap a Tool so its execution goes through a Temporal activity. + + The original function is stashed in activities._original_tool_fns + so the activity can find and call it. The tool's .fn is replaced + with a wrapper that calls workflow.execute_activity(). + + Returns a *new* Tool object — the original is not mutated. + """ + # Stash the original so the activity can find it + register_original_fn(t.name, t.fn) + + tool_name = t.name # capture for closure + + async def activity_calling_fn(**kwargs: Any) -> Any: + return await workflow.execute_activity( + tool_call_activity, + ToolCallParams(tool_name=tool_name, tool_args=kwargs), + start_to_close_timeout=timedelta(minutes=2), + ) + + wrapped = ai.Tool( + name=t.name, + description=t.description, + schema=t.schema, + fn=activity_calling_fn, + ) + + # HACK: replace the global registry entry so ai.execute_tool() finds + # the wrapped version. The activity uses _original_tool_fns to find + # the real function. This is the "dumbest version" — we'll extract + # a proper framework primitive once the overall shape settles. + _tool_registry[t.name] = wrapped + + return wrapped + + +def temporal_tools(tools: list[ai.Tool]) -> list[ai.Tool]: + """Wrap a list of tools for durable execution.""" + return [temporal_tool(t) for t in tools] diff --git a/examples/temporal-durable/main.py b/examples/temporal-durable/main.py new file mode 100644 index 00000000..3d12f6b2 --- /dev/null +++ b/examples/temporal-durable/main.py @@ -0,0 +1,79 @@ +"""Entry point — starts a Temporal worker and executes the agent workflow. + +For simplicity, the worker and client run in the same process. In production +you'd run the worker separately and use the client to start workflows. + +Prerequisites: + 1. Temporal dev server running: 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 + +from temporalio.client import Client +from temporalio.worker import Worker +from temporalio.worker.workflow_sandbox import ( + SandboxRestrictions, + SandboxedWorkflowRunner, +) + +from activities import llm_stream_activity, tool_call_activity +from workflow import AgentWorkflow + +TASK_QUEUE = "agent-durable" + + +async def main(user_query: str) -> None: + # Connect to local Temporal server + client = await Client.connect("localhost:7233") + + # Pass pydantic and vercel_ai_sdk modules through the sandbox so they + # don't get re-imported in the restricted environment. + sandbox_restrictions = SandboxRestrictions.default.with_passthrough_modules( + "pydantic", + "pydantic_core", + "vercel_ai_sdk", + ) + + # Start the worker in the background — it picks up workflow and + # activity tasks from the queue. + async with Worker( + client, + task_queue=TASK_QUEUE, + workflows=[AgentWorkflow], + activities=[llm_stream_activity, tool_call_activity], + workflow_runner=SandboxedWorkflowRunner(restrictions=sandbox_restrictions), + ): + # Execute the workflow and wait for the result. + # Each execution gets a unique ID so we can re-run freely. + workflow_id = f"agent-{uuid.uuid4().hex[:8]}" + + print(f"Starting workflow {workflow_id}") + print(f"Query: {user_query}") + print() + + result = await client.execute_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/pyproject.toml b/examples/temporal-durable/pyproject.toml new file mode 100644 index 00000000..dcd7b894 --- /dev/null +++ b/examples/temporal-durable/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "temporal-durable" +version = "0.1.0" +description = "Durable agent execution with Temporal" +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/tools.py b/examples/temporal-durable/tools.py new file mode 100644 index 00000000..08520e72 --- /dev/null +++ b/examples/temporal-durable/tools.py @@ -0,0 +1,22 @@ +"""Tool definitions — identical to the non-durable version. + +These are plain @ai.tool functions. They know nothing about Temporal. +The durable.temporal_tool() wrapper handles routing their execution +through activities. +""" + +import vercel_ai_sdk as ai + + +@ai.tool +async def get_weather(city: str) -> str: + """Get current weather for a city.""" + return f"Sunny, 72F in {city}" + + +@ai.tool +async def get_population(city: str) -> int: + """Get population of a city.""" + return {"new york": 8_336_817, "los angeles": 3_979_576}.get( + city.lower(), 1_000_000 + ) diff --git a/examples/temporal-durable/uv.lock b/examples/temporal-durable/uv.lock new file mode 100644 index 00000000..f4dc0c58 --- /dev/null +++ b/examples/temporal-durable/uv.lock @@ -0,0 +1,900 @@ +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 new file mode 100644 index 00000000..ee97137b --- /dev/null +++ b/examples/temporal-durable/workflow.py @@ -0,0 +1,92 @@ +"""Temporal workflow — the durable agent loop. + +The agent function is essentially identical to custom_loop.py. The only +differences are: + + 1. The LLM is a TemporalLanguageModel (activity-backed) + 2. The tools are wrapped with temporal_tool() (activity-backed) + 3. ai.run() drains fully inside the workflow (no streaming out) + +Everything else — ai.run(), @ai.stream, ai.execute_tool, asyncio.gather +for parallel tool calls — works unchanged inside the Temporal workflow. +""" + +from __future__ import annotations + +import asyncio + +from temporalio import workflow + +# Pass our modules through the Temporal sandbox — they don't need +# to be re-imported on each replay. This is required because the +# sandbox would otherwise try to re-exec them in a restricted env +# where network imports etc. are blocked. +with workflow.unsafe.imports_passed_through(): + import vercel_ai_sdk as ai + from durable import TemporalLanguageModel, temporal_tools + from tools import get_population, get_weather + + +# ── The agent function — same as custom_loop.py ────────────────── + + +async def agent(llm: ai.LanguageModel, user_query: str) -> ai.StreamResult: + """Custom agent loop with manual tool execution. + + This is the same logic as examples/samples/custom_loop.py. + The only difference is that llm and tools are temporal-wrapped, + so all I/O goes through durable activities. + """ + tools = temporal_tools([get_weather, get_population]) + messages = ai.make_messages( + system="Answer questions using the weather and population tools.", + user=user_query, + ) + + while True: + result = await ai.stream_step(llm, messages, tools, label="agent") + + if not result.tool_calls: + return result + + messages.append(result.last_message) + await asyncio.gather( + *( + ai.execute_tool(tc, message=result.last_message) + for tc in result.tool_calls + ) + ) + + +# ── Temporal workflow definition ───────────────────────────────── + + +@workflow.defn +class AgentWorkflow: + """Durable agent workflow. + + The workflow body drives ai.run() which pumps the Runtime event loop. + All I/O (LLM calls, tool calls) is routed through Temporal activities + by the TemporalLanguageModel and temporal_tools wrappers. + + On replay after a crash, Temporal replays the activity results from + its event history — the workflow re-executes deterministically and + each workflow.execute_activity() returns the cached result. + """ + + @workflow.run + async def run(self, user_query: str) -> str: + llm = TemporalLanguageModel() + + # ai.run() creates a Runtime, pumps the step queue, yields messages. + # Inside a workflow we can't stream out, so we drain everything and + # return the final text. + run_result = ai.run(agent, llm, user_query) + + final_text = "" + async for msg in run_result: + # Collect the last non-empty text — that's the final answer + if msg.text: + final_text = msg.text + + return final_text From 1cef86f1819ccf560b798ef78dea1680aa36c476 Mon Sep 17 00:00:00 2001 From: Andrey Buzin Date: Fri, 13 Feb 2026 09:37:54 -0800 Subject: [PATCH 2/5] Refactor the durable example, add a zero-framework example for reference --- examples/temporal-durable/README.md | 110 ++++++---------- examples/temporal-durable/activities.py | 118 ----------------- examples/temporal-durable/durable.py | 122 ----------------- examples/temporal-durable/pyproject.toml | 1 + examples/temporal-durable/raw/activities.py | 102 +++++++++++++++ examples/temporal-durable/raw/main.py | 55 ++++++++ examples/temporal-durable/raw/workflow.py | 91 +++++++++++++ examples/temporal-durable/tools.py | 22 ---- examples/temporal-durable/uv.lock | 2 + .../temporal-durable/with_sdk/activities.py | 99 ++++++++++++++ .../temporal-durable/{ => with_sdk}/main.py | 33 ++--- .../temporal-durable/with_sdk/workflow.py | 123 ++++++++++++++++++ examples/temporal-durable/workflow.py | 92 ------------- 13 files changed, 523 insertions(+), 447 deletions(-) delete mode 100644 examples/temporal-durable/activities.py delete mode 100644 examples/temporal-durable/durable.py create mode 100644 examples/temporal-durable/raw/activities.py create mode 100644 examples/temporal-durable/raw/main.py create mode 100644 examples/temporal-durable/raw/workflow.py delete mode 100644 examples/temporal-durable/tools.py create mode 100644 examples/temporal-durable/with_sdk/activities.py rename examples/temporal-durable/{ => with_sdk}/main.py (51%) create mode 100644 examples/temporal-durable/with_sdk/workflow.py delete mode 100644 examples/temporal-durable/workflow.py diff --git a/examples/temporal-durable/README.md b/examples/temporal-durable/README.md index 2abb3bcc..df4a62d9 100644 --- a/examples/temporal-durable/README.md +++ b/examples/temporal-durable/README.md @@ -1,92 +1,62 @@ # Durable Agent Execution with Temporal -Reproduces the `custom_loop.py` sample with Temporal durable execution. -LLM calls and tool calls are Temporal activities; the agent loop is a -Temporal workflow. The whole thing survives crashes and restarts. +Two implementations of the same agent (weather + population tools) as a +Temporal workflow. Both survive crashes and restarts — every LLM call and +tool call is a durable activity that Temporal replays from history. -## How it works +## `with_sdk/` — Using vercel-ai-sdk -``` -workflow.py durable.py activities.py -┌──────────┐ ┌───────────────────┐ ┌──────────────────┐ -│ ai.run() │───>│TemporalLanguage │───>│llm_stream_activity│ -│ agent() │ │Model.stream() │ │ (real LLM call) │ -│ │ │ execute_activity()│ │ │ -│ │ ├───────────────────┤ ├──────────────────┤ -│ execute_ │───>│temporal_tool().fn │───>│tool_call_activity │ -│ tool() │ │ execute_activity()│ │ (real tool call) │ -└──────────┘ └───────────────────┘ └──────────────────┘ - WORKFLOW WRAPPERS ACTIVITIES (I/O) - (deterministic) (routing) (non-deterministic) -``` +Same agent loop as `examples/samples/custom_loop.py`, but with: +- `TemporalLanguageModel` — wraps `llm.stream()` in an activity +- Tool calls routed through activities via `execute_tool_via_activity()` +- `ai.run()`, `ai.stream_step()`, `ai.make_messages()` all work unchanged -- **Workflow** (`workflow.py`): The agent loop runs inside `ai.run()`, unchanged - from the non-durable version. Uses `asyncio.TaskGroup`, `Queue`, `Future` — all - fine inside Temporal's custom event loop. -- **Wrappers** (`durable.py`): `TemporalLanguageModel` and `temporal_tool()` replace - real I/O with `workflow.execute_activity()` calls. On replay, Temporal returns - cached results. -- **Activities** (`activities.py`): Real I/O happens here — LLM API calls and tool - function execution. Activities are retried automatically on failure. +**3 files:** `activities.py` (tools + I/O), `workflow.py` (loop + wrappers), `main.py` -## Setup +## `raw/` — No framework -### 1. Install Temporal CLI +The same agent as plain Python + Temporal + anthropic SDK. No framework. +The entire agent loop is ~30 lines of dict manipulation. -```bash -brew install temporal # macOS -# or: curl -sSf https://temporal.download/cli.sh | sh -``` +**3 files:** `activities.py` (tools + I/O), `workflow.py` (loop), `main.py` -### 2. Start Temporal dev server +## Setup ```bash +# 1. Install & start Temporal +brew install temporal temporal server start-dev -``` - -This starts a local server on `localhost:7233` with a UI at `http://localhost:8233`. -### 3. Install dependencies - -```bash +# 2. Install deps cd examples/temporal-durable uv sync -``` -### 4. Set environment variables +# 3. Set API key (both examples use AI Gateway) +export AI_GATEWAY_API_KEY=... -```bash -export AI_GATEWAY_API_KEY=your-key-here +# 4. Run +uv run python with_sdk/main.py +uv run python raw/main.py ``` -### 5. Run +## How it works -```bash -uv run python main.py -# or with a custom query: -uv run python main.py "What is the weather in Tokyo?" +``` +Workflow (deterministic) Activities (real I/O) +┌─────────────────────────┐ ┌──────────────────────┐ +│ while True: │ │ │ +│ response = activity───┼─────────>│ llm_call(messages) │ +│ │<─────────┼ → Anthropic API │ +│ if no tool_calls: │ │ │ +│ return text │ │ │ +│ │ │ │ +│ gather( │ │ │ +│ activity(tool1) ────┼─────────>│ tool_call(name,args)│ +│ activity(tool2) ────┼─────────>│ tool_call(name,args)│ +│ ) │<─────────┼ → plain functions │ +└─────────────────────────┘ └──────────────────────┘ ``` -## Architecture notes - -### Streaming limitation - -Temporal activities are request/response — they can't stream. The -`TemporalLanguageModel` buffers the full LLM response inside the activity -and returns it as a single result. The workflow sees a "stream" that -completes in one shot. - -### Tool registry hack - -`temporal_tool()` replaces the global tool registry entry with the -activity-calling wrapper, and stashes the original function for the -activity to use. This is the "dumbest version" — a proper framework -primitive will replace this once the overall shape of durable execution -settles. - -### Sandbox passthrough - -Temporal's workflow sandbox re-imports modules on each replay. We use -`workflow.unsafe.imports_passed_through()` to pass `vercel_ai_sdk` and -our local modules through unchanged, since they don't do non-deterministic -things at import time. +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. diff --git a/examples/temporal-durable/activities.py b/examples/temporal-durable/activities.py deleted file mode 100644 index cc1f5098..00000000 --- a/examples/temporal-durable/activities.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Temporal activities — all real I/O lives here. - -Two activities: - 1. llm_stream_activity — calls the real LLM, drains the stream, returns messages - 2. tool_call_activity — calls the real tool function, returns the result - -These run in the activity context (outside the workflow sandbox), so they can -do network I/O, access environment variables, etc. -""" - -from __future__ import annotations - -import os -from dataclasses import dataclass -from typing import Any - -from temporalio import activity - -import vercel_ai_sdk as ai -from vercel_ai_sdk.anthropic import AnthropicModel - - -# ── Serializable parameter / result types ──────────────────────── - - -@dataclass -class LLMCallParams: - """Input to the LLM activity. All fields are JSON-serializable.""" - - messages: list[dict[str, Any]] # [Message.model_dump(), ...] - tool_schemas: list[dict[str, Any]] # [{name, description, schema}, ...] - - -@dataclass -class LLMCallResult: - """Output from the LLM activity.""" - - messages: list[dict[str, Any]] # final accumulated messages from the stream - - -@dataclass -class ToolCallParams: - """Input to the tool call activity.""" - - tool_name: str - tool_args: dict[str, Any] - - -# ── Stash for original tool functions ──────────────────────────── -# temporal_tool() puts originals here before replacing .fn with an -# activity-calling wrapper. The activity looks them up from here. - -_original_tool_fns: dict[str, Any] = {} - - -def register_original_fn(name: str, fn: Any) -> None: - _original_tool_fns[name] = fn - - -def get_original_fn(name: str) -> Any: - return _original_tool_fns[name] - - -# ── Activities ─────────────────────────────────────────────────── - - -@activity.defn(name="llm_stream") -async def llm_stream_activity(params: LLMCallParams) -> LLMCallResult: - """Call the LLM, drain the full stream, return the final messages. - - The real AnthropicModel is constructed here from environment variables. - The stream is consumed fully — the workflow only sees the final result. - """ - # Hardcoded for the example — in production, model config would be - # passed through or resolved from a registry. - llm = AnthropicModel( - model="anthropic/claude-sonnet-4", - base_url="https://ai-gateway.vercel.sh", - api_key=os.environ.get("AI_GATEWAY_API_KEY"), - ) - - # Reconstruct Message objects from serialized dicts - messages = [ai.Message.model_validate(m) for m in params.messages] - - # Reconstruct Tool objects (schema-only, for the LLM to know what's available) - tools = [ - ai.Tool( - name=t["name"], - description=t["description"], - schema=t["schema"], - fn=lambda: None, # placeholder — not called, just for schema - ) - for t in params.tool_schemas - ] - - # Drain the stream, keep all emitted messages - result_messages: list[ai.Message] = [] - async for msg in llm.stream(messages=messages, tools=tools or None): - result_messages.append(msg) - - # Only return the final message (the one with is_done=True and all parts settled) - # Earlier messages are intermediate streaming states of the same message. - final = result_messages[-1] if result_messages else None - if final is None: - return LLMCallResult(messages=[]) - - return LLMCallResult(messages=[final.model_dump()]) - - -@activity.defn(name="tool_call") -async def tool_call_activity(params: ToolCallParams) -> Any: - """Execute a tool function by name with the given arguments. - - Looks up the *original* function (not the temporal wrapper) from the - stash populated by temporal_tool(). - """ - fn = get_original_fn(params.tool_name) - return await fn(**params.tool_args) diff --git a/examples/temporal-durable/durable.py b/examples/temporal-durable/durable.py deleted file mode 100644 index edeaa4ec..00000000 --- a/examples/temporal-durable/durable.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Durable execution primitives — thin wrappers that route I/O through Temporal activities. - -Two primitives: - 1. TemporalLanguageModel — LanguageModel whose stream() calls an activity - 2. temporal_tool() — wraps a Tool so its fn calls an activity - -The agent code stays identical to the non-durable version. These wrappers -are the only things that know about Temporal. -""" - -from __future__ import annotations - -from collections.abc import AsyncGenerator -from datetime import timedelta -from typing import Any, override - -from temporalio import workflow -from temporalio.common import RetryPolicy - -import vercel_ai_sdk as ai -from vercel_ai_sdk.core.tools import _tool_registry - -from activities import ( - LLMCallParams, - LLMCallResult, - ToolCallParams, - llm_stream_activity, - register_original_fn, - tool_call_activity, -) - - -# ── TemporalLanguageModel ──────────────────────────────────────── - - -class TemporalLanguageModel(ai.LanguageModel): - """LanguageModel that delegates to a Temporal activity for each LLM call. - - Instead of hitting the real API, stream() calls workflow.execute_activity() - which runs the real LLM call inside an activity (with retry, timeout, etc.). - - The activity drains the full stream and returns the final messages. We - yield them as-if they arrived all at once — the "buffering wrapper" that - bridges streaming to Temporal's request/response model. - """ - - @override - async def stream( - self, - messages: list[ai.Message], - tools: list[ai.Tool] | None = None, - ) -> AsyncGenerator[ai.Message, None]: - # Serialize for the activity boundary - tool_schemas = [ - {"name": t.name, "description": t.description, "schema": t.schema} - for t in (tools or []) - ] - - params = LLMCallParams( - messages=[m.model_dump() for m in messages], - tool_schemas=tool_schemas, - ) - - # Call the activity — this is the durable boundary. - # On replay, Temporal returns the cached result without re-executing. - result: LLMCallResult = await workflow.execute_activity( - llm_stream_activity, - params, - start_to_close_timeout=timedelta(minutes=5), - retry_policy=RetryPolicy(maximum_attempts=3), - ) - - # Yield the final message(s) — the @ai.stream decorator and - # Runtime pump see this as a (very fast) stream that completes - # in one shot. - for msg_dict in result.messages: - yield ai.Message.model_validate(msg_dict) - - -# ── temporal_tool ──────────────────────────────────────────────── - - -def temporal_tool(t: ai.Tool) -> ai.Tool: - """Wrap a Tool so its execution goes through a Temporal activity. - - The original function is stashed in activities._original_tool_fns - so the activity can find and call it. The tool's .fn is replaced - with a wrapper that calls workflow.execute_activity(). - - Returns a *new* Tool object — the original is not mutated. - """ - # Stash the original so the activity can find it - register_original_fn(t.name, t.fn) - - tool_name = t.name # capture for closure - - async def activity_calling_fn(**kwargs: Any) -> Any: - return await workflow.execute_activity( - tool_call_activity, - ToolCallParams(tool_name=tool_name, tool_args=kwargs), - start_to_close_timeout=timedelta(minutes=2), - ) - - wrapped = ai.Tool( - name=t.name, - description=t.description, - schema=t.schema, - fn=activity_calling_fn, - ) - - # HACK: replace the global registry entry so ai.execute_tool() finds - # the wrapped version. The activity uses _original_tool_fns to find - # the real function. This is the "dumbest version" — we'll extract - # a proper framework primitive once the overall shape settles. - _tool_registry[t.name] = wrapped - - return wrapped - - -def temporal_tools(tools: list[ai.Tool]) -> list[ai.Tool]: - """Wrap a list of tools for durable execution.""" - return [temporal_tool(t) for t in tools] diff --git a/examples/temporal-durable/pyproject.toml b/examples/temporal-durable/pyproject.toml index dcd7b894..3c1373dd 100644 --- a/examples/temporal-durable/pyproject.toml +++ b/examples/temporal-durable/pyproject.toml @@ -5,6 +5,7 @@ description = "Durable agent execution with Temporal" requires-python = ">=3.12" dependencies = [ "vercel-ai-sdk", + "anthropic>=0.40.0", "temporalio>=1.9.0", ] diff --git a/examples/temporal-durable/raw/activities.py b/examples/temporal-durable/raw/activities.py new file mode 100644 index 00000000..1769e188 --- /dev/null +++ b/examples/temporal-durable/raw/activities.py @@ -0,0 +1,102 @@ +"""Temporal activities — all real I/O lives here. + +Uses the anthropic SDK directly. No framework. +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from typing import Any + +import anthropic +from temporalio import activity + +MODEL = "anthropic/claude-sonnet-4" + + +# ── Tool functions ─────────────────────────────────────────────── + + +def get_weather(city: str) -> str: + return f"Sunny, 72F in {city}" + + +def get_population(city: str) -> int: + return {"new york": 8_336_817, "los angeles": 3_979_576}.get( + city.lower(), 1_000_000 + ) + + +TOOL_FNS = { + "get_weather": get_weather, + "get_population": get_population, +} + +# Anthropic tool schemas — what the LLM sees. +TOOL_SCHEMAS = [ + { + "name": "get_weather", + "description": "Get current weather for a city.", + "input_schema": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + }, + }, + { + "name": "get_population", + "description": "Get population of a city.", + "input_schema": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + }, + }, +] + + +# ── Serializable parameter types ───────────────────────────────── + + +@dataclass +class LLMCallParams: + messages: list[dict[str, Any]] + + +@dataclass +class LLMCallResult: + response: dict[str, Any] # raw Anthropic response + + +@dataclass +class ToolCallParams: + tool_name: str + tool_args: dict[str, Any] + + +# ── Activities ─────────────────────────────────────────────────── + + +@activity.defn(name="llm_call") +async def llm_call_activity(params: LLMCallParams) -> LLMCallResult: + """Call the Anthropic API, return the full response.""" + client = anthropic.AsyncAnthropic( + base_url="https://ai-gateway.vercel.sh", + api_key=os.environ.get("AI_GATEWAY_API_KEY"), + ) + response = await client.messages.create( + model=MODEL, + max_tokens=1024, + system="Answer questions using the weather and population tools.", + messages=params.messages, + tools=TOOL_SCHEMAS, + ) + return LLMCallResult(response=response.model_dump()) + + +@activity.defn(name="tool_call") +async def tool_call_activity(params: ToolCallParams) -> Any: + """Execute a tool function by name.""" + fn = TOOL_FNS[params.tool_name] + return fn(**params.tool_args) diff --git a/examples/temporal-durable/raw/main.py b/examples/temporal-durable/raw/main.py new file mode 100644 index 00000000..3714c760 --- /dev/null +++ b/examples/temporal-durable/raw/main.py @@ -0,0 +1,55 @@ +"""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 raw/main.py + uv run python raw/main.py "What is the weather in Tokyo?" +""" + +from __future__ import annotations + +import asyncio +import sys +import uuid + +from temporalio.client import Client +from temporalio.worker import Worker + +from activities import llm_call_activity, tool_call_activity +from workflow import AgentWorkflow + +TASK_QUEUE = "agent-raw" + + +async def main(user_query: str) -> None: + client = await Client.connect("localhost:7233") + + async with Worker( + client, + task_queue=TASK_QUEUE, + workflows=[AgentWorkflow], + activities=[llm_call_activity, tool_call_activity], + ): + workflow_id = f"agent-raw-{uuid.uuid4().hex[:8]}" + print(f"Workflow {workflow_id}") + print(f"Query: {user_query}\n") + + result = await client.execute_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/raw/workflow.py b/examples/temporal-durable/raw/workflow.py new file mode 100644 index 00000000..7b1ed72e --- /dev/null +++ b/examples/temporal-durable/raw/workflow.py @@ -0,0 +1,91 @@ +"""Temporal workflow — the durable agent loop. + +No framework. Plain dicts for messages, raw Anthropic response format. +The entire agent loop is ~30 lines. +""" + +from __future__ import annotations + +import asyncio +from datetime import timedelta +from typing import Any + +from temporalio import workflow +from temporalio.common import RetryPolicy + +with workflow.unsafe.imports_passed_through(): + from activities import ( + LLMCallParams, + LLMCallResult, + ToolCallParams, + llm_call_activity, + tool_call_activity, + ) + + +@workflow.defn +class AgentWorkflow: + @workflow.run + async def run(self, user_query: str) -> str: + messages: list[dict[str, Any]] = [ + {"role": "user", "content": user_query}, + ] + + while True: + # ── LLM call (durable) ─────────────────────────── + result: LLMCallResult = await workflow.execute_activity( + llm_call_activity, + LLMCallParams(messages=messages), + start_to_close_timeout=timedelta(minutes=5), + retry_policy=RetryPolicy(maximum_attempts=3), + ) + response = result.response + + # Append assistant message + messages.append( + { + "role": "assistant", + "content": response["content"], + } + ) + + # Extract tool_use blocks + tool_calls = [ + block for block in response["content"] if block["type"] == "tool_use" + ] + + if not tool_calls: + # No tools — extract final text and return + text_blocks = [ + block["text"] + for block in response["content"] + if block["type"] == "text" + ] + return "\n".join(text_blocks) + + # ── Parallel tool execution (each call is durable) ─ + tool_results = await asyncio.gather( + *( + workflow.execute_activity( + tool_call_activity, + ToolCallParams(tool_name=tc["name"], tool_args=tc["input"]), + start_to_close_timeout=timedelta(minutes=2), + ) + for tc in tool_calls + ) + ) + + # Append tool results as a single user message + messages.append( + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": tc["id"], + "content": str(result), + } + for tc, result in zip(tool_calls, tool_results) + ], + } + ) diff --git a/examples/temporal-durable/tools.py b/examples/temporal-durable/tools.py deleted file mode 100644 index 08520e72..00000000 --- a/examples/temporal-durable/tools.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Tool definitions — identical to the non-durable version. - -These are plain @ai.tool functions. They know nothing about Temporal. -The durable.temporal_tool() wrapper handles routing their execution -through activities. -""" - -import vercel_ai_sdk as ai - - -@ai.tool -async def get_weather(city: str) -> str: - """Get current weather for a city.""" - return f"Sunny, 72F in {city}" - - -@ai.tool -async def get_population(city: str) -> int: - """Get population of a city.""" - return {"new york": 8_336_817, "los angeles": 3_979_576}.get( - city.lower(), 1_000_000 - ) diff --git a/examples/temporal-durable/uv.lock b/examples/temporal-durable/uv.lock index f4dc0c58..ab57b8a3 100644 --- a/examples/temporal-durable/uv.lock +++ b/examples/temporal-durable/uv.lock @@ -723,12 +723,14 @@ name = "temporal-durable" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "anthropic" }, { name = "temporalio" }, { name = "vercel-ai-sdk" }, ] [package.metadata] requires-dist = [ + { name = "anthropic", specifier = ">=0.40.0" }, { name = "temporalio", specifier = ">=1.9.0" }, { name = "vercel-ai-sdk", editable = "../../" }, ] diff --git a/examples/temporal-durable/with_sdk/activities.py b/examples/temporal-durable/with_sdk/activities.py new file mode 100644 index 00000000..240837ed --- /dev/null +++ b/examples/temporal-durable/with_sdk/activities.py @@ -0,0 +1,99 @@ +"""Temporal activities — all real I/O lives here. + +Activities run outside the workflow sandbox, so they can do network I/O, +access environment variables, etc. +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from typing import Any + +from temporalio import activity + +import vercel_ai_sdk as ai +from vercel_ai_sdk.anthropic import AnthropicModel + + +# ── Tool functions ─────────────────────────────────────────────── +# Same as examples/samples/custom_loop.py — plain @ai.tool functions. + + +@ai.tool +async def get_weather(city: str) -> str: + """Get current weather for a city.""" + return f"Sunny, 72F in {city}" + + +@ai.tool +async def get_population(city: str) -> int: + """Get population of a city.""" + return {"new york": 8_336_817, "los angeles": 3_979_576}.get( + city.lower(), 1_000_000 + ) + + +TOOLS: dict[str, ai.Tool] = { + "get_weather": get_weather, + "get_population": get_population, +} + + +# ── Serializable parameter types ───────────────────────────────── + + +@dataclass +class LLMCallParams: + messages: list[dict[str, Any]] + tool_schemas: list[dict[str, Any]] + + +@dataclass +class LLMCallResult: + messages: list[dict[str, Any]] + + +@dataclass +class ToolCallParams: + tool_name: str + tool_args: dict[str, Any] + + +# ── Activities ─────────────────────────────────────────────────── + + +@activity.defn(name="llm_stream") +async def llm_stream_activity(params: LLMCallParams) -> LLMCallResult: + """Call the LLM, drain the stream, return the final message.""" + llm = AnthropicModel( + model="anthropic/claude-sonnet-4", + base_url="https://ai-gateway.vercel.sh", + api_key=os.environ.get("AI_GATEWAY_API_KEY"), + ) + + messages = [ai.Message.model_validate(m) for m in params.messages] + tools = [ + ai.Tool( + name=t["name"], + description=t["description"], + schema=t["schema"], + fn=lambda: None, + ) + for t in params.tool_schemas + ] + + final = None + async for msg in llm.stream(messages=messages, tools=tools or None): + final = msg + + if final is None: + return LLMCallResult(messages=[]) + return LLMCallResult(messages=[final.model_dump()]) + + +@activity.defn(name="tool_call") +async def tool_call_activity(params: ToolCallParams) -> Any: + """Execute a tool function by name.""" + tool = TOOLS[params.tool_name] + return await tool.fn(**params.tool_args) diff --git a/examples/temporal-durable/main.py b/examples/temporal-durable/with_sdk/main.py similarity index 51% rename from examples/temporal-durable/main.py rename to examples/temporal-durable/with_sdk/main.py index 3d12f6b2..a2e55294 100644 --- a/examples/temporal-durable/main.py +++ b/examples/temporal-durable/with_sdk/main.py @@ -1,15 +1,12 @@ """Entry point — starts a Temporal worker and executes the agent workflow. -For simplicity, the worker and client run in the same process. In production -you'd run the worker separately and use the client to start workflows. - Prerequisites: - 1. Temporal dev server running: temporal server start-dev + 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?" + uv run python with_sdk/main.py + uv run python with_sdk/main.py "What is the weather in Tokyo?" """ from __future__ import annotations @@ -28,37 +25,28 @@ from activities import llm_stream_activity, tool_call_activity from workflow import AgentWorkflow -TASK_QUEUE = "agent-durable" +TASK_QUEUE = "agent-sdk" async def main(user_query: str) -> None: - # Connect to local Temporal server client = await Client.connect("localhost:7233") - # Pass pydantic and vercel_ai_sdk modules through the sandbox so they - # don't get re-imported in the restricted environment. - sandbox_restrictions = SandboxRestrictions.default.with_passthrough_modules( + restrictions = SandboxRestrictions.default.with_passthrough_modules( "pydantic", "pydantic_core", "vercel_ai_sdk", ) - # Start the worker in the background — it picks up workflow and - # activity tasks from the queue. async with Worker( client, task_queue=TASK_QUEUE, workflows=[AgentWorkflow], activities=[llm_stream_activity, tool_call_activity], - workflow_runner=SandboxedWorkflowRunner(restrictions=sandbox_restrictions), + workflow_runner=SandboxedWorkflowRunner(restrictions=restrictions), ): - # Execute the workflow and wait for the result. - # Each execution gets a unique ID so we can re-run freely. - workflow_id = f"agent-{uuid.uuid4().hex[:8]}" - - print(f"Starting workflow {workflow_id}") - print(f"Query: {user_query}") - print() + workflow_id = f"agent-sdk-{uuid.uuid4().hex[:8]}" + print(f"Workflow {workflow_id}") + print(f"Query: {user_query}\n") result = await client.execute_workflow( AgentWorkflow.run, @@ -66,7 +54,6 @@ async def main(user_query: str) -> None: id=workflow_id, task_queue=TASK_QUEUE, ) - print(result) @@ -74,6 +61,6 @@ async def main(user_query: str) -> None: query = ( sys.argv[1] if len(sys.argv) > 1 - else "What's the weather and population of New York and Los Angeles?" + else ("What's the weather and population of New York and Los Angeles?") ) asyncio.run(main(query)) diff --git a/examples/temporal-durable/with_sdk/workflow.py b/examples/temporal-durable/with_sdk/workflow.py new file mode 100644 index 00000000..a17f2b03 --- /dev/null +++ b/examples/temporal-durable/with_sdk/workflow.py @@ -0,0 +1,123 @@ +"""Temporal workflow — the durable agent loop. + +Same logic as examples/samples/custom_loop.py. The differences: + 1. LLM calls go through a Temporal activity (TemporalLanguageModel) + 2. Tool calls go through a Temporal activity (direct execute_activity) + 3. ai.run() drains fully inside the workflow (no streaming out) +""" + +from __future__ import annotations + +import asyncio +import json +from collections.abc import AsyncGenerator +from datetime import timedelta +from typing import Any, override + +from temporalio import workflow +from temporalio.common import RetryPolicy + +with workflow.unsafe.imports_passed_through(): + import vercel_ai_sdk as ai + + from activities import ( + TOOLS, + LLMCallParams, + LLMCallResult, + ToolCallParams, + llm_stream_activity, + tool_call_activity, + ) + + +# ── TemporalLanguageModel ──────────────────────────────────────── + + +class TemporalLanguageModel(ai.LanguageModel): + """LanguageModel that delegates to a Temporal activity. + + The activity drains the full LLM stream and returns the final message. + On replay, Temporal returns the cached result without re-executing. + """ + + @override + async def stream( + self, + messages: list[ai.Message], + tools: list[ai.Tool] | None = None, + ) -> AsyncGenerator[ai.Message, None]: + tool_schemas = [ + {"name": t.name, "description": t.description, "schema": t.schema} + for t in (tools or []) + ] + result: LLMCallResult = await workflow.execute_activity( + llm_stream_activity, + LLMCallParams( + messages=[m.model_dump() for m in messages], + tool_schemas=tool_schemas, + ), + start_to_close_timeout=timedelta(minutes=5), + retry_policy=RetryPolicy(maximum_attempts=3), + ) + for msg_dict in result.messages: + yield ai.Message.model_validate(msg_dict) + + +# ── Tool execution via activity ────────────────────────────────── + + +async def execute_tool_via_activity( + tool_call: ai.ToolPart, message: ai.Message +) -> None: + """Execute a tool call through a Temporal activity, then update the ToolPart in-place.""" + args = json.loads(tool_call.tool_args) + result = await workflow.execute_activity( + tool_call_activity, + ToolCallParams(tool_name=tool_call.tool_name, tool_args=args), + start_to_close_timeout=timedelta(minutes=2), + ) + # ToolPart.result is typed as dict[str, Any] — wrap raw values so + # the message survives Pydantic serialize/deserialize at the activity boundary. + if not isinstance(result, dict): + result = {"value": result} + tool_call.set_result(result) + + +# ── Agent function ─────────────────────────────────────────────── + + +async def agent(llm: ai.LanguageModel, user_query: str) -> ai.StreamResult: + tools = list(TOOLS.values()) + messages = ai.make_messages( + system="Answer questions using the weather and population tools.", + user=user_query, + ) + + while True: + result = await ai.stream_step(llm, messages, tools, label="agent") + + if not result.tool_calls: + return result + + messages.append(result.last_message) + await asyncio.gather( + *( + execute_tool_via_activity(tc, result.last_message) + for tc in result.tool_calls + ) + ) + + +# ── Workflow ───────────────────────────────────────────────────── + + +@workflow.defn +class AgentWorkflow: + @workflow.run + async def run(self, user_query: str) -> str: + llm = TemporalLanguageModel() + final_text = "" + async for msg in ai.run(agent, llm, user_query): + if msg.text: + final_text = msg.text + return final_text diff --git a/examples/temporal-durable/workflow.py b/examples/temporal-durable/workflow.py deleted file mode 100644 index ee97137b..00000000 --- a/examples/temporal-durable/workflow.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Temporal workflow — the durable agent loop. - -The agent function is essentially identical to custom_loop.py. The only -differences are: - - 1. The LLM is a TemporalLanguageModel (activity-backed) - 2. The tools are wrapped with temporal_tool() (activity-backed) - 3. ai.run() drains fully inside the workflow (no streaming out) - -Everything else — ai.run(), @ai.stream, ai.execute_tool, asyncio.gather -for parallel tool calls — works unchanged inside the Temporal workflow. -""" - -from __future__ import annotations - -import asyncio - -from temporalio import workflow - -# Pass our modules through the Temporal sandbox — they don't need -# to be re-imported on each replay. This is required because the -# sandbox would otherwise try to re-exec them in a restricted env -# where network imports etc. are blocked. -with workflow.unsafe.imports_passed_through(): - import vercel_ai_sdk as ai - from durable import TemporalLanguageModel, temporal_tools - from tools import get_population, get_weather - - -# ── The agent function — same as custom_loop.py ────────────────── - - -async def agent(llm: ai.LanguageModel, user_query: str) -> ai.StreamResult: - """Custom agent loop with manual tool execution. - - This is the same logic as examples/samples/custom_loop.py. - The only difference is that llm and tools are temporal-wrapped, - so all I/O goes through durable activities. - """ - tools = temporal_tools([get_weather, get_population]) - messages = ai.make_messages( - system="Answer questions using the weather and population tools.", - user=user_query, - ) - - while True: - result = await ai.stream_step(llm, messages, tools, label="agent") - - if not result.tool_calls: - return result - - messages.append(result.last_message) - await asyncio.gather( - *( - ai.execute_tool(tc, message=result.last_message) - for tc in result.tool_calls - ) - ) - - -# ── Temporal workflow definition ───────────────────────────────── - - -@workflow.defn -class AgentWorkflow: - """Durable agent workflow. - - The workflow body drives ai.run() which pumps the Runtime event loop. - All I/O (LLM calls, tool calls) is routed through Temporal activities - by the TemporalLanguageModel and temporal_tools wrappers. - - On replay after a crash, Temporal replays the activity results from - its event history — the workflow re-executes deterministically and - each workflow.execute_activity() returns the cached result. - """ - - @workflow.run - async def run(self, user_query: str) -> str: - llm = TemporalLanguageModel() - - # ai.run() creates a Runtime, pumps the step queue, yields messages. - # Inside a workflow we can't stream out, so we drain everything and - # return the final text. - run_result = ai.run(agent, llm, user_query) - - final_text = "" - async for msg in run_result: - # Collect the last non-empty text — that's the final answer - if msg.text: - final_text = msg.text - - return final_text From 542d33fc61e35a0e8c67c5f2666055a8110321dc Mon Sep 17 00:00:00 2001 From: Andrey Buzin Date: Fri, 13 Feb 2026 10:59:43 -0800 Subject: [PATCH 3/5] Simplify the framework-y durable example --- examples/temporal-durable/README.md | 27 +++- .../temporal-durable/with_sdk/activities.py | 38 +++-- examples/temporal-durable/with_sdk/main.py | 15 +- .../temporal-durable/with_sdk/workflow.py | 145 +++++++++--------- src/vercel_ai_sdk/core/messages.py | 2 +- 5 files changed, 118 insertions(+), 109 deletions(-) diff --git a/examples/temporal-durable/README.md b/examples/temporal-durable/README.md index df4a62d9..b4e1264e 100644 --- a/examples/temporal-durable/README.md +++ b/examples/temporal-durable/README.md @@ -6,12 +6,19 @@ tool call is a durable activity that Temporal replays from history. ## `with_sdk/` — Using vercel-ai-sdk -Same agent loop as `examples/samples/custom_loop.py`, but with: -- `TemporalLanguageModel` — wraps `llm.stream()` in an activity -- Tool calls routed through activities via `execute_tool_via_activity()` -- `ai.run()`, `ai.stream_step()`, `ai.make_messages()` all work unchanged +The framework and Temporal compose via plain async/await: -**3 files:** `activities.py` (tools + I/O), `workflow.py` (loop + wrappers), `main.py` +- **Tools**: `@ai.tool` with `execute_activity()` in the body — each tool + invocation is automatically a durable activity +- **LLM**: `buffered_model()` wraps an activity call into a `LanguageModel` +- **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` ## `raw/` — No framework @@ -60,3 +67,13 @@ Workflow (deterministic) Activities (real I/O) 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. + +## Framework helpers used in `with_sdk/` + +| Pattern | What it replaces | +|---|---| +| `@ai.tool` with activity body | Manual `TOOL_SCHEMAS` dicts + `TOOL_FNS` dispatch table | +| `buffered_model(fn)` | Hand-written `LanguageModel` subclass (28 lines → 5) | +| `ai.stream_loop(llm, msgs, tools)` | Manual while/gather/append loop | +| `ai.make_messages(system=..., user=...)` | Raw dict construction | +| `ai.Message` (typed, Pydantic) | `dict[str, Any]` blobs | diff --git a/examples/temporal-durable/with_sdk/activities.py b/examples/temporal-durable/with_sdk/activities.py index 240837ed..b77e2f0f 100644 --- a/examples/temporal-durable/with_sdk/activities.py +++ b/examples/temporal-durable/with_sdk/activities.py @@ -1,7 +1,8 @@ """Temporal activities — all real I/O lives here. Activities run outside the workflow sandbox, so they can do network I/O, -access environment variables, etc. +access environment variables, etc. No framework imports needed — these +are plain functions wrapped with @activity.defn. """ from __future__ import annotations @@ -12,29 +13,24 @@ from temporalio import activity -import vercel_ai_sdk as ai from vercel_ai_sdk.anthropic import AnthropicModel +import vercel_ai_sdk as ai -# ── Tool functions ─────────────────────────────────────────────── -# Same as examples/samples/custom_loop.py — plain @ai.tool functions. +# ── Tool functions (plain Python) ───────────────────────────────── -@ai.tool -async def get_weather(city: str) -> str: - """Get current weather for a city.""" +def get_weather(city: str) -> str: return f"Sunny, 72F in {city}" -@ai.tool -async def get_population(city: str) -> int: - """Get population of a city.""" +def get_population(city: str) -> int: return {"new york": 8_336_817, "los angeles": 3_979_576}.get( city.lower(), 1_000_000 ) -TOOLS: dict[str, ai.Tool] = { +TOOL_FNS: dict[str, Any] = { "get_weather": get_weather, "get_population": get_population, } @@ -51,7 +47,7 @@ class LLMCallParams: @dataclass class LLMCallResult: - messages: list[dict[str, Any]] + messages: list[dict[str, Any]] # list of serialized ai.Message @dataclass @@ -63,8 +59,8 @@ class ToolCallParams: # ── Activities ─────────────────────────────────────────────────── -@activity.defn(name="llm_stream") -async def llm_stream_activity(params: LLMCallParams) -> LLMCallResult: +@activity.defn(name="llm_call") +async def llm_call_activity(params: LLMCallParams) -> LLMCallResult: """Call the LLM, drain the stream, return the final message.""" llm = AnthropicModel( model="anthropic/claude-sonnet-4", @@ -73,12 +69,14 @@ async def llm_stream_activity(params: LLMCallParams) -> LLMCallResult: ) messages = [ai.Message.model_validate(m) for m in params.messages] + + # Build Tool objects with schema only (fn is not called activity-side). + # TODO: framework should expose ToolSchema or let stream() accept dicts. + async def _noop() -> None: ... + tools = [ ai.Tool( - name=t["name"], - description=t["description"], - schema=t["schema"], - fn=lambda: None, + name=t["name"], description=t["description"], schema=t["schema"], fn=_noop ) for t in params.tool_schemas ] @@ -95,5 +93,5 @@ async def llm_stream_activity(params: LLMCallParams) -> LLMCallResult: @activity.defn(name="tool_call") async def tool_call_activity(params: ToolCallParams) -> Any: """Execute a tool function by name.""" - tool = TOOLS[params.tool_name] - return await tool.fn(**params.tool_args) + fn = TOOL_FNS[params.tool_name] + return fn(**params.tool_args) diff --git a/examples/temporal-durable/with_sdk/main.py b/examples/temporal-durable/with_sdk/main.py index a2e55294..decc111b 100644 --- a/examples/temporal-durable/with_sdk/main.py +++ b/examples/temporal-durable/with_sdk/main.py @@ -17,12 +17,8 @@ from temporalio.client import Client from temporalio.worker import Worker -from temporalio.worker.workflow_sandbox import ( - SandboxRestrictions, - SandboxedWorkflowRunner, -) -from activities import llm_stream_activity, tool_call_activity +from activities import llm_call_activity, tool_call_activity from workflow import AgentWorkflow TASK_QUEUE = "agent-sdk" @@ -31,18 +27,11 @@ async def main(user_query: str) -> None: client = await Client.connect("localhost:7233") - restrictions = SandboxRestrictions.default.with_passthrough_modules( - "pydantic", - "pydantic_core", - "vercel_ai_sdk", - ) - async with Worker( client, task_queue=TASK_QUEUE, workflows=[AgentWorkflow], - activities=[llm_stream_activity, tool_call_activity], - workflow_runner=SandboxedWorkflowRunner(restrictions=restrictions), + activities=[llm_call_activity, tool_call_activity], ): workflow_id = f"agent-sdk-{uuid.uuid4().hex[:8]}" print(f"Workflow {workflow_id}") diff --git a/examples/temporal-durable/with_sdk/workflow.py b/examples/temporal-durable/with_sdk/workflow.py index a17f2b03..f753724e 100644 --- a/examples/temporal-durable/with_sdk/workflow.py +++ b/examples/temporal-durable/with_sdk/workflow.py @@ -1,16 +1,14 @@ """Temporal workflow — the durable agent loop. -Same logic as examples/samples/custom_loop.py. The differences: - 1. LLM calls go through a Temporal activity (TemporalLanguageModel) - 2. Tool calls go through a Temporal activity (direct execute_activity) - 3. ai.run() drains fully inside the workflow (no streaming out) +Key insight: @ai.tool and ai.stream_loop work unchanged — the tool +*bodies* call execute_activity, and the LLM is a buffered_model that +calls an activity. The framework doesn't know about Temporal; Temporal +doesn't know about the framework. They compose via plain async/await. """ from __future__ import annotations -import asyncio -import json -from collections.abc import AsyncGenerator +from collections.abc import AsyncGenerator, Awaitable, Callable from datetime import timedelta from typing import Any, override @@ -21,91 +19,90 @@ import vercel_ai_sdk as ai from activities import ( - TOOLS, LLMCallParams, LLMCallResult, ToolCallParams, - llm_stream_activity, + llm_call_activity, tool_call_activity, ) -# ── TemporalLanguageModel ──────────────────────────────────────── - - -class TemporalLanguageModel(ai.LanguageModel): - """LanguageModel that delegates to a Temporal activity. - - The activity drains the full LLM stream and returns the final message. - On replay, Temporal returns the cached result without re-executing. - """ - - @override - async def stream( - self, - messages: list[ai.Message], - tools: list[ai.Tool] | None = None, - ) -> AsyncGenerator[ai.Message, None]: - tool_schemas = [ - {"name": t.name, "description": t.description, "schema": t.schema} - for t in (tools or []) - ] - result: LLMCallResult = await workflow.execute_activity( - llm_stream_activity, - LLMCallParams( - messages=[m.model_dump() for m in messages], - tool_schemas=tool_schemas, - ), - start_to_close_timeout=timedelta(minutes=5), - retry_policy=RetryPolicy(maximum_attempts=3), - ) - for msg_dict in result.messages: - yield ai.Message.model_validate(msg_dict) +# ── buffered_model (candidate for framework extraction) ────────── +# +# Wraps an async callable into a LanguageModel. The callable receives +# serialized messages + tool schemas and returns serialized messages. +# This lets you slot in any durable execution backend (Temporal, Inngest, +# etc.) without subclassing LanguageModel by hand. +# +# TODO: move to ai.buffered_model() in the framework. + + +def buffered_model( + call_fn: Callable[[LLMCallParams], Awaitable[LLMCallResult]], +) -> ai.LanguageModel: + """Create a LanguageModel that delegates to an async callable.""" + + class _Buffered(ai.LanguageModel): + @override + async def stream( + self, + messages: list[ai.Message], + tools: list[ai.Tool] | None = None, + ) -> AsyncGenerator[ai.Message, None]: + tool_schemas = [ + {"name": t.name, "description": t.description, "schema": t.schema} + for t in (tools or []) + ] + result = await call_fn( + LLMCallParams( + messages=[m.model_dump() for m in messages], + tool_schemas=tool_schemas, + ) + ) + for msg_dict in result.messages: + yield ai.Message.model_validate(msg_dict) + + return _Buffered() + +# ── 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. The framework's +# execute_tool() calls tool.fn(), which just does the activity call. -# ── Tool execution via activity ────────────────────────────────── + +@ai.tool +async def get_weather(city: str) -> str: + """Get current weather for a city.""" + return await workflow.execute_activity( + tool_call_activity, + ToolCallParams(tool_name="get_weather", tool_args={"city": city}), + start_to_close_timeout=timedelta(minutes=2), + ) -async def execute_tool_via_activity( - tool_call: ai.ToolPart, message: ai.Message -) -> None: - """Execute a tool call through a Temporal activity, then update the ToolPart in-place.""" - args = json.loads(tool_call.tool_args) - result = await workflow.execute_activity( +@ai.tool +async def get_population(city: str) -> int: + """Get population of a city.""" + return await workflow.execute_activity( tool_call_activity, - ToolCallParams(tool_name=tool_call.tool_name, tool_args=args), + ToolCallParams(tool_name="get_population", tool_args={"city": city}), start_to_close_timeout=timedelta(minutes=2), ) - # ToolPart.result is typed as dict[str, Any] — wrap raw values so - # the message survives Pydantic serialize/deserialize at the activity boundary. - if not isinstance(result, dict): - result = {"value": result} - tool_call.set_result(result) -# ── Agent function ─────────────────────────────────────────────── +# ── Agent ──────────────────────────────────────────────────────── async def agent(llm: ai.LanguageModel, user_query: str) -> ai.StreamResult: - tools = list(TOOLS.values()) + """Agent loop — identical to the non-Temporal version.""" messages = ai.make_messages( system="Answer questions using the weather and population tools.", user=user_query, ) - - while True: - result = await ai.stream_step(llm, messages, tools, label="agent") - - if not result.tool_calls: - return result - - messages.append(result.last_message) - await asyncio.gather( - *( - execute_tool_via_activity(tc, result.last_message) - for tc in result.tool_calls - ) - ) + return await ai.stream_loop(llm, messages, [get_weather, get_population]) # ── Workflow ───────────────────────────────────────────────────── @@ -115,7 +112,15 @@ async def agent(llm: ai.LanguageModel, user_query: str) -> ai.StreamResult: class AgentWorkflow: @workflow.run async def run(self, user_query: str) -> str: - llm = TemporalLanguageModel() + llm = buffered_model( + lambda params: workflow.execute_activity( + llm_call_activity, + params, + start_to_close_timeout=timedelta(minutes=5), + retry_policy=RetryPolicy(maximum_attempts=3), + ) + ) + final_text = "" async for msg in ai.run(agent, llm, user_query): if msg.text: diff --git a/src/vercel_ai_sdk/core/messages.py b/src/vercel_ai_sdk/core/messages.py index 1131c304..b8f5dd6d 100644 --- a/src/vercel_ai_sdk/core/messages.py +++ b/src/vercel_ai_sdk/core/messages.py @@ -23,7 +23,7 @@ class ToolPart(pydantic.BaseModel): tool_name: str tool_args: str status: Literal["pending", "result"] = "pending" # Execution status - result: dict[str, Any] | None = None + result: Any = None type: Literal["tool"] = "tool" # Streaming state (for args streaming) state: PartState | None = None From 42cf0609f28d1207049895f7fb9f4a50dca236eb Mon Sep 17 00:00:00 2001 From: Andrey Buzin Date: Fri, 13 Feb 2026 12:01:29 -0800 Subject: [PATCH 4/5] Split ToolSchema and Tool, simplify the durable example further --- examples/temporal-durable/README.md | 14 ++-- .../temporal-durable/with_sdk/activities.py | 63 ++++------------ examples/temporal-durable/with_sdk/main.py | 4 +- .../temporal-durable/with_sdk/workflow.py | 72 ++++++++----------- src/vercel_ai_sdk/__init__.py | 3 +- src/vercel_ai_sdk/anthropic/__init__.py | 10 +-- src/vercel_ai_sdk/core/llm.py | 19 ++++- src/vercel_ai_sdk/core/runtime.py | 6 +- src/vercel_ai_sdk/core/tools.py | 32 ++++++--- src/vercel_ai_sdk/mcp/client.py | 2 +- src/vercel_ai_sdk/openai/__init__.py | 10 +-- tests/core/test_tools.py | 18 ++--- 12 files changed, 119 insertions(+), 134 deletions(-) diff --git a/examples/temporal-durable/README.md b/examples/temporal-durable/README.md index b4e1264e..c498933f 100644 --- a/examples/temporal-durable/README.md +++ b/examples/temporal-durable/README.md @@ -9,8 +9,9 @@ tool call is a durable activity that Temporal replays from history. The framework and Temporal compose via plain async/await: - **Tools**: `@ai.tool` with `execute_activity()` in the body — each tool - invocation is automatically a durable activity -- **LLM**: `buffered_model()` wraps an activity call into a `LanguageModel` + is its own activity with a matching signature, no dispatch table needed +- **LLM**: `buffered_model()` wraps an activity call into a `LanguageModel`; + the activity uses `llm.buffer()` and `ToolSchema` (no dummy `fn`, no drain loop) - **Loop**: `ai.stream_loop()` runs the agent loop unchanged - **Bus**: `ai.run()` provides the unified message bus for streaming @@ -58,8 +59,8 @@ Workflow (deterministic) Activities (real I/O) │ return text │ │ │ │ │ │ │ │ gather( │ │ │ -│ activity(tool1) ────┼─────────>│ tool_call(name,args)│ -│ activity(tool2) ────┼─────────>│ tool_call(name,args)│ +│ activity(tool1) ────┼─────────>│ get_weather(city) │ +│ activity(tool2) ────┼─────────>│ get_population(city)│ │ ) │<─────────┼ → plain functions │ └─────────────────────────┘ └──────────────────────┘ ``` @@ -68,12 +69,13 @@ 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. -## Framework helpers used in `with_sdk/` +## Framework primitives used in `with_sdk/` | Pattern | What it replaces | |---|---| | `@ai.tool` with activity body | Manual `TOOL_SCHEMAS` dicts + `TOOL_FNS` dispatch table | -| `buffered_model(fn)` | Hand-written `LanguageModel` subclass (28 lines → 5) | +| `ai.ToolSchema` | `Tool(fn=_noop)` hacks for passing schemas across boundaries | +| `llm.buffer(messages, tools)` | Manual stream drain loop | | `ai.stream_loop(llm, msgs, tools)` | Manual while/gather/append loop | | `ai.make_messages(system=..., user=...)` | Raw dict construction | | `ai.Message` (typed, Pydantic) | `dict[str, Any]` blobs | diff --git a/examples/temporal-durable/with_sdk/activities.py b/examples/temporal-durable/with_sdk/activities.py index b77e2f0f..29191669 100644 --- a/examples/temporal-durable/with_sdk/activities.py +++ b/examples/temporal-durable/with_sdk/activities.py @@ -1,8 +1,8 @@ """Temporal activities — all real I/O lives here. -Activities run outside the workflow sandbox, so they can do network I/O, -access environment variables, etc. No framework imports needed — these -are plain functions wrapped with @activity.defn. +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 @@ -13,30 +13,26 @@ from temporalio import activity -from vercel_ai_sdk.anthropic import AnthropicModel import vercel_ai_sdk as ai +from vercel_ai_sdk.anthropic import AnthropicModel -# ── Tool functions (plain Python) ───────────────────────────────── +# ── Tool activities (one per tool, plain functions) ─────────────── -def get_weather(city: str) -> str: +@activity.defn(name="get_weather") +async def get_weather_activity(city: str) -> str: return f"Sunny, 72F in {city}" -def get_population(city: str) -> int: +@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 ) -TOOL_FNS: dict[str, Any] = { - "get_weather": get_weather, - "get_population": get_population, -} - - -# ── Serializable parameter types ───────────────────────────────── +# ── LLM activity ───────────────────────────────────────────────── @dataclass @@ -47,16 +43,7 @@ class LLMCallParams: @dataclass class LLMCallResult: - messages: list[dict[str, Any]] # list of serialized ai.Message - - -@dataclass -class ToolCallParams: - tool_name: str - tool_args: dict[str, Any] - - -# ── Activities ─────────────────────────────────────────────────── + message: dict[str, Any] # serialized ai.Message @activity.defn(name="llm_call") @@ -69,29 +56,7 @@ async def llm_call_activity(params: LLMCallParams) -> LLMCallResult: ) messages = [ai.Message.model_validate(m) for m in params.messages] + tools = [ai.ToolSchema.model_validate(t) for t in params.tool_schemas] - # Build Tool objects with schema only (fn is not called activity-side). - # TODO: framework should expose ToolSchema or let stream() accept dicts. - async def _noop() -> None: ... - - tools = [ - ai.Tool( - name=t["name"], description=t["description"], schema=t["schema"], fn=_noop - ) - for t in params.tool_schemas - ] - - final = None - async for msg in llm.stream(messages=messages, tools=tools or None): - final = msg - - if final is None: - return LLMCallResult(messages=[]) - return LLMCallResult(messages=[final.model_dump()]) - - -@activity.defn(name="tool_call") -async def tool_call_activity(params: ToolCallParams) -> Any: - """Execute a tool function by name.""" - fn = TOOL_FNS[params.tool_name] - return fn(**params.tool_args) + result = await llm.buffer(messages, tools) + return LLMCallResult(message=result.model_dump()) diff --git a/examples/temporal-durable/with_sdk/main.py b/examples/temporal-durable/with_sdk/main.py index decc111b..ca516a33 100644 --- a/examples/temporal-durable/with_sdk/main.py +++ b/examples/temporal-durable/with_sdk/main.py @@ -18,7 +18,7 @@ from temporalio.client import Client from temporalio.worker import Worker -from activities import llm_call_activity, tool_call_activity +from activities import get_weather_activity, get_population_activity, llm_call_activity from workflow import AgentWorkflow TASK_QUEUE = "agent-sdk" @@ -31,7 +31,7 @@ async def main(user_query: str) -> None: client, task_queue=TASK_QUEUE, workflows=[AgentWorkflow], - activities=[llm_call_activity, tool_call_activity], + activities=[llm_call_activity, get_weather_activity, get_population_activity], ): workflow_id = f"agent-sdk-{uuid.uuid4().hex[:8]}" print(f"Workflow {workflow_id}") diff --git a/examples/temporal-durable/with_sdk/workflow.py b/examples/temporal-durable/with_sdk/workflow.py index f753724e..72378d49 100644 --- a/examples/temporal-durable/with_sdk/workflow.py +++ b/examples/temporal-durable/with_sdk/workflow.py @@ -8,9 +8,9 @@ from __future__ import annotations -from collections.abc import AsyncGenerator, Awaitable, Callable +from collections.abc import AsyncGenerator, Awaitable, Callable, Sequence from datetime import timedelta -from typing import Any, override +from typing import override from temporalio import workflow from temporalio.common import RetryPolicy @@ -21,64 +21,54 @@ from activities import ( LLMCallParams, LLMCallResult, - ToolCallParams, + get_population_activity, + get_weather_activity, llm_call_activity, - tool_call_activity, ) # ── buffered_model (candidate for framework extraction) ────────── # # Wraps an async callable into a LanguageModel. The callable receives -# serialized messages + tool schemas and returns serialized messages. +# serialized messages + tool schemas and returns a serialized message. # This lets you slot in any durable execution backend (Temporal, Inngest, # etc.) without subclassing LanguageModel by hand. -# -# TODO: move to ai.buffered_model() in the framework. - - -def buffered_model( - call_fn: Callable[[LLMCallParams], Awaitable[LLMCallResult]], -) -> ai.LanguageModel: - """Create a LanguageModel that delegates to an async callable.""" - - class _Buffered(ai.LanguageModel): - @override - async def stream( - self, - messages: list[ai.Message], - tools: list[ai.Tool] | None = None, - ) -> AsyncGenerator[ai.Message, None]: - tool_schemas = [ - {"name": t.name, "description": t.description, "schema": t.schema} - for t in (tools or []) - ] - result = await call_fn( - LLMCallParams( - messages=[m.model_dump() for m in messages], - tool_schemas=tool_schemas, - ) - ) - for msg_dict in result.messages: - yield ai.Message.model_validate(msg_dict) - return _Buffered() + +class DurableModel(ai.LanguageModel): + def __init__( + self, call_fn: Callable[[LLMCallParams], Awaitable[LLMCallResult]] + ) -> None: + self.call_fn = call_fn + + @override + async def stream( + self, + messages: list[ai.Message], + tools: Sequence[ai.ToolSchema] | None = None, + ) -> AsyncGenerator[ai.Message, None]: + result = await self.call_fn( + LLMCallParams( + messages=[m.model_dump() for m in messages], + tool_schemas=[t.model_dump() 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. The framework's -# execute_tool() calls tool.fn(), which just does the activity call. +# invocation a durable Temporal activity. @ai.tool async def get_weather(city: str) -> str: """Get current weather for a city.""" return await workflow.execute_activity( - tool_call_activity, - ToolCallParams(tool_name="get_weather", tool_args={"city": city}), + get_weather_activity, + args=[city], start_to_close_timeout=timedelta(minutes=2), ) @@ -87,8 +77,8 @@ async def get_weather(city: str) -> str: async def get_population(city: str) -> int: """Get population of a city.""" return await workflow.execute_activity( - tool_call_activity, - ToolCallParams(tool_name="get_population", tool_args={"city": city}), + get_population_activity, + args=[city], start_to_close_timeout=timedelta(minutes=2), ) @@ -112,7 +102,7 @@ async def agent(llm: ai.LanguageModel, user_query: str) -> ai.StreamResult: class AgentWorkflow: @workflow.run async def run(self, user_query: str) -> str: - llm = buffered_model( + llm = DurableModel( lambda params: workflow.execute_activity( llm_call_activity, params, diff --git a/src/vercel_ai_sdk/__init__.py b/src/vercel_ai_sdk/__init__.py index 322460e0..fc25bbce 100644 --- a/src/vercel_ai_sdk/__init__.py +++ b/src/vercel_ai_sdk/__init__.py @@ -12,7 +12,7 @@ HookPart, make_messages, ) -from .core.tools import Tool, tool +from .core.tools import ToolSchema, Tool, tool from .core.llm import LanguageModel from .core.streams import StreamResult, stream from .core.runtime import ( @@ -37,6 +37,7 @@ "ToolPart", "ToolDelta", "ReasoningPart", + "ToolSchema", "Tool", "LanguageModel", "Runtime", diff --git a/src/vercel_ai_sdk/anthropic/__init__.py b/src/vercel_ai_sdk/anthropic/__init__.py index 21e29fd3..1d6281c3 100644 --- a/src/vercel_ai_sdk/anthropic/__init__.py +++ b/src/vercel_ai_sdk/anthropic/__init__.py @@ -2,7 +2,7 @@ import json import os -from collections.abc import AsyncGenerator +from collections.abc import AsyncGenerator, Sequence from typing import Any, override import anthropic @@ -10,13 +10,13 @@ from .. import core -def _tools_to_anthropic(tools: list[core.tools.Tool]) -> list[dict[str, Any]]: +def _tools_to_anthropic(tools: Sequence[core.tools.ToolSchema]) -> list[dict[str, Any]]: """Convert internal Tool objects to Anthropic tool schema format.""" return [ { "name": tool.name, "description": tool.description, - "input_schema": tool.schema, + "input_schema": tool.tool_schema, } for tool in tools ] @@ -117,7 +117,7 @@ def __init__( async def stream_events( self, messages: list[core.messages.Message], - tools: list[core.tools.Tool] | None = None, + tools: Sequence[core.tools.ToolSchema] | None = None, ) -> AsyncGenerator[core.llm.StreamEvent, None]: """Yield raw stream events from Anthropic API.""" system_prompt, anthropic_messages = _messages_to_anthropic(messages) @@ -206,7 +206,7 @@ async def stream_events( async def stream( self, messages: list[core.messages.Message], - tools: list[core.tools.Tool] | None = None, + tools: Sequence[core.tools.ToolSchema] | None = None, ) -> AsyncGenerator[core.messages.Message, None]: """Stream Messages (uses StreamProcessor internally).""" handler = core.llm.StreamHandler() diff --git a/src/vercel_ai_sdk/core/llm.py b/src/vercel_ai_sdk/core/llm.py index 5cfce165..8a402fee 100644 --- a/src/vercel_ai_sdk/core/llm.py +++ b/src/vercel_ai_sdk/core/llm.py @@ -2,7 +2,7 @@ import abc import dataclasses -from collections.abc import AsyncGenerator +from collections.abc import AsyncGenerator, Sequence from . import messages as messages_ from . import tools as tools_ @@ -213,7 +213,22 @@ def _build_message( class LanguageModel(abc.ABC): @abc.abstractmethod async def stream( - self, messages: list[messages_.Message], tools: list[tools_.Tool] | None = None + self, + messages: list[messages_.Message], + tools: Sequence[tools_.ToolSchema] | None = None, ) -> AsyncGenerator[messages_.Message, None]: raise NotImplementedError yield + + async def buffer( + self, + messages: list[messages_.Message], + tools: Sequence[tools_.ToolSchema] | None = None, + ) -> messages_.Message: + """Drain the stream and return the final message.""" + final = None + async for msg in self.stream(messages, tools): + final = msg + if final is None: + raise ValueError("LLM produced no messages") + return final diff --git a/src/vercel_ai_sdk/core/runtime.py b/src/vercel_ai_sdk/core/runtime.py index 4f1a1440..b055b9f7 100644 --- a/src/vercel_ai_sdk/core/runtime.py +++ b/src/vercel_ai_sdk/core/runtime.py @@ -4,7 +4,7 @@ import json import contextvars import dataclasses -from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine +from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine, Sequence from typing import Any, get_type_hints from .. import mcp @@ -187,7 +187,7 @@ def _find_runtime_param(fn: Callable[..., Any]) -> str | None: async def stream_step( llm: llm_.LanguageModel, messages: list[messages_.Message], - tools: list[tools_.Tool] | None = None, + tools: Sequence[tools_.ToolSchema] | None = None, label: str | None = None, ) -> AsyncGenerator[messages_.Message, None]: """Single LLM call that streams to Runtime.""" @@ -250,7 +250,7 @@ async def execute_tool( async def stream_loop( llm: llm_.LanguageModel, messages: list[messages_.Message], - tools: list[tools_.Tool], + tools: Sequence[tools_.ToolSchema], label: str | None = None, ) -> streams_.StreamResult: """Agent loop: stream LLM, execute tools, repeat until done.""" diff --git a/src/vercel_ai_sdk/core/tools.py b/src/vercel_ai_sdk/core/tools.py index 7c6c41bf..ce1a474f 100644 --- a/src/vercel_ai_sdk/core/tools.py +++ b/src/vercel_ai_sdk/core/tools.py @@ -1,6 +1,5 @@ from __future__ import annotations -import dataclasses import inspect from collections.abc import Awaitable from typing import Any, Callable, get_args, get_origin, get_type_hints, overload @@ -41,6 +40,27 @@ def _is_optional(param_type: type) -> bool: return False +class ToolSchema(pydantic.BaseModel): + """What the LLM sees: name, description, and JSON Schema for parameters. + + This is the serializable, function-free description of a tool. + Use this when you need tool metadata without the callable (e.g. passing + tool schemas across a serialization boundary to an LLM activity). + """ + + name: str + description: str + tool_schema: dict[str, Any] + + +class Tool(ToolSchema): + """A ToolSchema plus the async callable that implements it.""" + + fn: Callable[..., Awaitable[Any]] = pydantic.Field(exclude=True) + + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + + @overload def tool(fn: Callable[..., Awaitable[Any]]) -> Tool: ... @@ -84,7 +104,7 @@ def make_tool(f: Callable[..., Awaitable[Any]]) -> Tool: t = Tool( name=f.__name__, description=inspect.getdoc(f) or "", - schema=parameters, + tool_schema=parameters, fn=f, ) # Register in global registry @@ -94,11 +114,3 @@ def make_tool(f: Callable[..., Awaitable[Any]]) -> Tool: if fn is not None: return make_tool(fn) return make_tool - - -@dataclasses.dataclass -class Tool: - name: str - description: str - schema: dict[str, Any] - fn: Callable[..., Awaitable[Any]] diff --git a/src/vercel_ai_sdk/mcp/client.py b/src/vercel_ai_sdk/mcp/client.py index a8647cb3..ee8692a9 100644 --- a/src/vercel_ai_sdk/mcp/client.py +++ b/src/vercel_ai_sdk/mcp/client.py @@ -234,7 +234,7 @@ def _mcp_tool_to_native( return core.tools.Tool( name=name, description=mcp_tool.description or "", - schema=mcp_tool.inputSchema, + tool_schema=mcp_tool.inputSchema, fn=_make_tool_fn(connection_key, mcp_tool.name, transport_factory), ) diff --git a/src/vercel_ai_sdk/openai/__init__.py b/src/vercel_ai_sdk/openai/__init__.py index 4cad224e..7975b3b6 100644 --- a/src/vercel_ai_sdk/openai/__init__.py +++ b/src/vercel_ai_sdk/openai/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from collections.abc import AsyncGenerator +from collections.abc import AsyncGenerator, Sequence from typing import Any, override import openai @@ -9,7 +9,7 @@ from .. import core -def _tools_to_openai(tools: list[core.tools.Tool]) -> list[dict[str, Any]]: +def _tools_to_openai(tools: Sequence[core.tools.ToolSchema]) -> list[dict[str, Any]]: """Convert internal Tool objects to OpenAI tool schema format.""" return [ { @@ -17,7 +17,7 @@ def _tools_to_openai(tools: list[core.tools.Tool]) -> list[dict[str, Any]]: "function": { "name": tool.name, "description": tool.description, - "parameters": tool.schema, + "parameters": tool.tool_schema, }, } for tool in tools @@ -132,7 +132,7 @@ def __init__( async def stream_events( self, messages: list[core.messages.Message], - tools: list[core.tools.Tool] | None = None, + tools: Sequence[core.tools.ToolSchema] | None = None, ) -> AsyncGenerator[core.llm.StreamEvent, None]: """Yield raw stream events from OpenAI API.""" openai_messages = _messages_to_openai(messages) @@ -243,7 +243,7 @@ async def stream_events( async def stream( self, messages: list[core.messages.Message], - tools: list[core.tools.Tool] | None = None, + tools: Sequence[core.tools.ToolSchema] | None = None, ) -> AsyncGenerator[core.messages.Message, None]: """Stream Messages (uses StreamHandler internally).""" handler = core.llm.StreamHandler() diff --git a/tests/core/test_tools.py b/tests/core/test_tools.py index 6216986d..3923802b 100644 --- a/tests/core/test_tools.py +++ b/tests/core/test_tools.py @@ -20,10 +20,10 @@ async def greet(name: str, count: int) -> str: assert greet.name == "greet" assert greet.description == "Say hello." - props = greet.schema["properties"] + props = greet.tool_schema["properties"] assert props["name"] == {"type": "string"} assert props["count"] == {"type": "integer"} - assert set(greet.schema["required"]) == {"name", "count"} + assert set(greet.tool_schema["required"]) == {"name", "count"} def test_optional_param_not_required(): @@ -32,10 +32,10 @@ async def search(query: str, limit: Optional[int] = None) -> str: """Search.""" return query - assert "query" in search.schema.get("required", []) - assert "limit" not in search.schema.get("required", []) + assert "query" in search.tool_schema.get("required", []) + assert "limit" not in search.tool_schema.get("required", []) # limit should still appear in properties - assert "limit" in search.schema["properties"] + assert "limit" in search.tool_schema["properties"] def test_default_value_not_required(): @@ -54,7 +54,7 @@ async def send(recipients: list[str], urgent: bool = False) -> str: """Send message.""" return "sent" - props = send.schema["properties"] + props = send.tool_schema["properties"] assert props["recipients"]["type"] == "array" assert props["recipients"]["items"]["type"] == "string" @@ -68,10 +68,10 @@ async def needs_runtime(query: str, rt: Runtime) -> str: """Tool that needs runtime.""" return query - props = needs_runtime.schema["properties"] + props = needs_runtime.tool_schema["properties"] assert "rt" not in props assert "query" in props - assert set(needs_runtime.schema.get("required", [])) == {"query"} + assert set(needs_runtime.tool_schema.get("required", [])) == {"query"} # -- Registry ------------------------------------------------------------- @@ -108,4 +108,4 @@ async def add(a: int, b: int) -> int: def search_required(tool: ai.Tool) -> list[str]: - return tool.schema.get("required", []) + return tool.tool_schema.get("required", []) From f9dc891f8e4a846fc13dd17c97fe359dd084fc2a Mon Sep 17 00:00:00 2001 From: Andrey Buzin Date: Fri, 13 Feb 2026 12:45:11 -0800 Subject: [PATCH 5/5] Remove the raw example --- examples/fastapi-vite/backend/uv.lock | 1 - examples/temporal-durable/README.md | 68 +++++------- .../{with_sdk => }/activities.py | 0 .../temporal-durable/{with_sdk => }/main.py | 8 +- examples/temporal-durable/pyproject.toml | 1 - examples/temporal-durable/raw/activities.py | 102 ------------------ examples/temporal-durable/raw/main.py | 55 ---------- examples/temporal-durable/raw/workflow.py | 91 ---------------- examples/temporal-durable/uv.lock | 2 - .../{with_sdk => }/workflow.py | 16 +-- 10 files changed, 29 insertions(+), 315 deletions(-) rename examples/temporal-durable/{with_sdk => }/activities.py (100%) rename examples/temporal-durable/{with_sdk => }/main.py (87%) delete mode 100644 examples/temporal-durable/raw/activities.py delete mode 100644 examples/temporal-durable/raw/main.py delete mode 100644 examples/temporal-durable/raw/workflow.py rename examples/temporal-durable/{with_sdk => }/workflow.py (82%) diff --git a/examples/fastapi-vite/backend/uv.lock b/examples/fastapi-vite/backend/uv.lock index 842af125..3af4dba3 100644 --- a/examples/fastapi-vite/backend/uv.lock +++ b/examples/fastapi-vite/backend/uv.lock @@ -1345,7 +1345,6 @@ dev = [ { name = "pytest-asyncio", specifier = ">=0.24" }, { name = "python-dotenv", specifier = ">=1.2.1" }, { name = "rich", specifier = ">=14.2.0" }, - { name = "textual", specifier = ">=3.2.0" }, ] [[package]] diff --git a/examples/temporal-durable/README.md b/examples/temporal-durable/README.md index c498933f..2e6e8400 100644 --- a/examples/temporal-durable/README.md +++ b/examples/temporal-durable/README.md @@ -1,17 +1,18 @@ # Durable Agent Execution with Temporal -Two implementations of the same agent (weather + population tools) as a -Temporal workflow. Both survive crashes and restarts — every LLM call and -tool call is a durable activity that Temporal replays from history. +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. -## `with_sdk/` — Using vercel-ai-sdk +## 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, no dispatch table needed -- **LLM**: `buffered_model()` wraps an activity call into a `LanguageModel`; - the activity uses `llm.buffer()` and `ToolSchema` (no dummy `fn`, no drain loop) + 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 @@ -21,34 +22,6 @@ about Temporal. **3 files:** `activities.py` (I/O), `workflow.py` (agent + wrappers), `main.py` -## `raw/` — No framework - -The same agent as plain Python + Temporal + anthropic SDK. No framework. -The entire agent loop is ~30 lines of dict manipulation. - -**3 files:** `activities.py` (tools + I/O), `workflow.py` (loop), `main.py` - -## 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 (both examples use AI Gateway) -export AI_GATEWAY_API_KEY=... - -# 4. Run -uv run python with_sdk/main.py -uv run python raw/main.py -``` - -## How it works - ``` Workflow (deterministic) Activities (real I/O) ┌─────────────────────────┐ ┌──────────────────────┐ @@ -69,13 +42,20 @@ 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. -## Framework primitives used in `with_sdk/` +## Setup + +```bash +# 1. Install & start Temporal +brew install temporal +temporal server start-dev + +# 2. Install deps +cd examples/temporal-durable +uv sync -| Pattern | What it replaces | -|---|---| -| `@ai.tool` with activity body | Manual `TOOL_SCHEMAS` dicts + `TOOL_FNS` dispatch table | -| `ai.ToolSchema` | `Tool(fn=_noop)` hacks for passing schemas across boundaries | -| `llm.buffer(messages, tools)` | Manual stream drain loop | -| `ai.stream_loop(llm, msgs, tools)` | Manual while/gather/append loop | -| `ai.make_messages(system=..., user=...)` | Raw dict construction | -| `ai.Message` (typed, Pydantic) | `dict[str, Any]` blobs | +# 3. Set API key +export AI_GATEWAY_API_KEY=... + +# 4. Run +uv run python main.py +``` diff --git a/examples/temporal-durable/with_sdk/activities.py b/examples/temporal-durable/activities.py similarity index 100% rename from examples/temporal-durable/with_sdk/activities.py rename to examples/temporal-durable/activities.py diff --git a/examples/temporal-durable/with_sdk/main.py b/examples/temporal-durable/main.py similarity index 87% rename from examples/temporal-durable/with_sdk/main.py rename to examples/temporal-durable/main.py index ca516a33..1beb6ffa 100644 --- a/examples/temporal-durable/with_sdk/main.py +++ b/examples/temporal-durable/main.py @@ -5,8 +5,8 @@ 2. AI_GATEWAY_API_KEY environment variable set Usage: - uv run python with_sdk/main.py - uv run python with_sdk/main.py "What is the weather in Tokyo?" + uv run python main.py + uv run python main.py "What is the weather in Tokyo?" """ from __future__ import annotations @@ -21,7 +21,7 @@ from activities import get_weather_activity, get_population_activity, llm_call_activity from workflow import AgentWorkflow -TASK_QUEUE = "agent-sdk" +TASK_QUEUE = "agent-durable" async def main(user_query: str) -> None: @@ -33,7 +33,7 @@ async def main(user_query: str) -> None: workflows=[AgentWorkflow], activities=[llm_call_activity, get_weather_activity, get_population_activity], ): - workflow_id = f"agent-sdk-{uuid.uuid4().hex[:8]}" + workflow_id = f"agent-durable-{uuid.uuid4().hex[:8]}" print(f"Workflow {workflow_id}") print(f"Query: {user_query}\n") diff --git a/examples/temporal-durable/pyproject.toml b/examples/temporal-durable/pyproject.toml index 3c1373dd..dcd7b894 100644 --- a/examples/temporal-durable/pyproject.toml +++ b/examples/temporal-durable/pyproject.toml @@ -5,7 +5,6 @@ description = "Durable agent execution with Temporal" requires-python = ">=3.12" dependencies = [ "vercel-ai-sdk", - "anthropic>=0.40.0", "temporalio>=1.9.0", ] diff --git a/examples/temporal-durable/raw/activities.py b/examples/temporal-durable/raw/activities.py deleted file mode 100644 index 1769e188..00000000 --- a/examples/temporal-durable/raw/activities.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Temporal activities — all real I/O lives here. - -Uses the anthropic SDK directly. No framework. -""" - -from __future__ import annotations - -import os -from dataclasses import dataclass -from typing import Any - -import anthropic -from temporalio import activity - -MODEL = "anthropic/claude-sonnet-4" - - -# ── Tool functions ─────────────────────────────────────────────── - - -def get_weather(city: str) -> str: - return f"Sunny, 72F in {city}" - - -def get_population(city: str) -> int: - return {"new york": 8_336_817, "los angeles": 3_979_576}.get( - city.lower(), 1_000_000 - ) - - -TOOL_FNS = { - "get_weather": get_weather, - "get_population": get_population, -} - -# Anthropic tool schemas — what the LLM sees. -TOOL_SCHEMAS = [ - { - "name": "get_weather", - "description": "Get current weather for a city.", - "input_schema": { - "type": "object", - "properties": {"city": {"type": "string"}}, - "required": ["city"], - }, - }, - { - "name": "get_population", - "description": "Get population of a city.", - "input_schema": { - "type": "object", - "properties": {"city": {"type": "string"}}, - "required": ["city"], - }, - }, -] - - -# ── Serializable parameter types ───────────────────────────────── - - -@dataclass -class LLMCallParams: - messages: list[dict[str, Any]] - - -@dataclass -class LLMCallResult: - response: dict[str, Any] # raw Anthropic response - - -@dataclass -class ToolCallParams: - tool_name: str - tool_args: dict[str, Any] - - -# ── Activities ─────────────────────────────────────────────────── - - -@activity.defn(name="llm_call") -async def llm_call_activity(params: LLMCallParams) -> LLMCallResult: - """Call the Anthropic API, return the full response.""" - client = anthropic.AsyncAnthropic( - base_url="https://ai-gateway.vercel.sh", - api_key=os.environ.get("AI_GATEWAY_API_KEY"), - ) - response = await client.messages.create( - model=MODEL, - max_tokens=1024, - system="Answer questions using the weather and population tools.", - messages=params.messages, - tools=TOOL_SCHEMAS, - ) - return LLMCallResult(response=response.model_dump()) - - -@activity.defn(name="tool_call") -async def tool_call_activity(params: ToolCallParams) -> Any: - """Execute a tool function by name.""" - fn = TOOL_FNS[params.tool_name] - return fn(**params.tool_args) diff --git a/examples/temporal-durable/raw/main.py b/examples/temporal-durable/raw/main.py deleted file mode 100644 index 3714c760..00000000 --- a/examples/temporal-durable/raw/main.py +++ /dev/null @@ -1,55 +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 raw/main.py - uv run python raw/main.py "What is the weather in Tokyo?" -""" - -from __future__ import annotations - -import asyncio -import sys -import uuid - -from temporalio.client import Client -from temporalio.worker import Worker - -from activities import llm_call_activity, tool_call_activity -from workflow import AgentWorkflow - -TASK_QUEUE = "agent-raw" - - -async def main(user_query: str) -> None: - client = await Client.connect("localhost:7233") - - async with Worker( - client, - task_queue=TASK_QUEUE, - workflows=[AgentWorkflow], - activities=[llm_call_activity, tool_call_activity], - ): - workflow_id = f"agent-raw-{uuid.uuid4().hex[:8]}" - print(f"Workflow {workflow_id}") - print(f"Query: {user_query}\n") - - result = await client.execute_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/raw/workflow.py b/examples/temporal-durable/raw/workflow.py deleted file mode 100644 index 7b1ed72e..00000000 --- a/examples/temporal-durable/raw/workflow.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Temporal workflow — the durable agent loop. - -No framework. Plain dicts for messages, raw Anthropic response format. -The entire agent loop is ~30 lines. -""" - -from __future__ import annotations - -import asyncio -from datetime import timedelta -from typing import Any - -from temporalio import workflow -from temporalio.common import RetryPolicy - -with workflow.unsafe.imports_passed_through(): - from activities import ( - LLMCallParams, - LLMCallResult, - ToolCallParams, - llm_call_activity, - tool_call_activity, - ) - - -@workflow.defn -class AgentWorkflow: - @workflow.run - async def run(self, user_query: str) -> str: - messages: list[dict[str, Any]] = [ - {"role": "user", "content": user_query}, - ] - - while True: - # ── LLM call (durable) ─────────────────────────── - result: LLMCallResult = await workflow.execute_activity( - llm_call_activity, - LLMCallParams(messages=messages), - start_to_close_timeout=timedelta(minutes=5), - retry_policy=RetryPolicy(maximum_attempts=3), - ) - response = result.response - - # Append assistant message - messages.append( - { - "role": "assistant", - "content": response["content"], - } - ) - - # Extract tool_use blocks - tool_calls = [ - block for block in response["content"] if block["type"] == "tool_use" - ] - - if not tool_calls: - # No tools — extract final text and return - text_blocks = [ - block["text"] - for block in response["content"] - if block["type"] == "text" - ] - return "\n".join(text_blocks) - - # ── Parallel tool execution (each call is durable) ─ - tool_results = await asyncio.gather( - *( - workflow.execute_activity( - tool_call_activity, - ToolCallParams(tool_name=tc["name"], tool_args=tc["input"]), - start_to_close_timeout=timedelta(minutes=2), - ) - for tc in tool_calls - ) - ) - - # Append tool results as a single user message - messages.append( - { - "role": "user", - "content": [ - { - "type": "tool_result", - "tool_use_id": tc["id"], - "content": str(result), - } - for tc, result in zip(tool_calls, tool_results) - ], - } - ) diff --git a/examples/temporal-durable/uv.lock b/examples/temporal-durable/uv.lock index ab57b8a3..f4dc0c58 100644 --- a/examples/temporal-durable/uv.lock +++ b/examples/temporal-durable/uv.lock @@ -723,14 +723,12 @@ name = "temporal-durable" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "anthropic" }, { name = "temporalio" }, { name = "vercel-ai-sdk" }, ] [package.metadata] requires-dist = [ - { name = "anthropic", specifier = ">=0.40.0" }, { name = "temporalio", specifier = ">=1.9.0" }, { name = "vercel-ai-sdk", editable = "../../" }, ] diff --git a/examples/temporal-durable/with_sdk/workflow.py b/examples/temporal-durable/workflow.py similarity index 82% rename from examples/temporal-durable/with_sdk/workflow.py rename to examples/temporal-durable/workflow.py index 72378d49..e7d42275 100644 --- a/examples/temporal-durable/with_sdk/workflow.py +++ b/examples/temporal-durable/workflow.py @@ -1,10 +1,4 @@ -"""Temporal workflow — the durable agent loop. - -Key insight: @ai.tool and ai.stream_loop work unchanged — the tool -*bodies* call execute_activity, and the LLM is a buffered_model that -calls an activity. The framework doesn't know about Temporal; Temporal -doesn't know about the framework. They compose via plain async/await. -""" +"""Temporal workflow — the durable agent loop.""" from __future__ import annotations @@ -27,14 +21,6 @@ ) -# ── buffered_model (candidate for framework extraction) ────────── -# -# Wraps an async callable into a LanguageModel. The callable receives -# serialized messages + tool schemas and returns a serialized message. -# This lets you slot in any durable execution backend (Temporal, Inngest, -# etc.) without subclassing LanguageModel by hand. - - class DurableModel(ai.LanguageModel): def __init__( self, call_fn: Callable[[LLMCallParams], Awaitable[LLMCallResult]]