diff --git a/docs/specs/004-python-function-calling-loop.md b/docs/specs/004-python-function-calling-loop.md index 8462dc76ac..ceede1ff73 100644 --- a/docs/specs/004-python-function-calling-loop.md +++ b/docs/specs/004-python-function-calling-loop.md @@ -372,6 +372,9 @@ that manually replay messages own the equivalent rule: do not resend an approval - Model-bound history contains one function call/result pair per completed logical occurrence. - Append-only history must not replay stale approval request/response wrappers to the model. - Framework-managed and service-managed continuation must preserve the same logical call/result transcript. +- A streaming response rebuilt from updates by an intermediate middleware must carry over the inner response's + conversation id and its internal-conversation-id marker, so framework-managed continuation appends only the latest + message instead of replaying a transcript the provider already holds. - A trusted terminal result consumes the corresponding approval authority in explicit stateless replay; a result in a server-registered pending occurrence cannot consume that authority before local execution. @@ -475,6 +478,7 @@ that manually replay messages own the equivalent rule: do not resend an approval | Pending hosted history replay | Stateless hosted approval requests remain replayable until a response is recorded, then both controls become inert. | `packages/openai/tests/openai/test_openai_chat_client.py::test_stateless_history_preserves_pending_hosted_approval_request_until_response` | | Non-history provider plus session | Local history is still auto-injected for approval resume. | `packages/core/tests/core/test_agents.py::test_non_history_context_provider_still_injects_inmemory` | | Hosted per-service-call persistence | A host-managed transcript remains available throughout a local function-call loop without being persisted into the framework session and replayed on the next hosted request. | `packages/foundry_hosting/tests/test_responses.py::TestAgentSessionPersistence::test_per_service_call_persistence_preserves_function_loop_history` | +| Streaming message injection with per-service-call persistence | A streaming response rebuilt from updates retains the inner conversation id and its internal marker, so the next iteration appends only the latest message rather than replaying the whole turn on top of provider-held history. | `packages/core/tests/core/test_middleware_with_chat.py::TestChatMiddleware::test_message_injection_middleware_streaming_preserves_inner_continuation_state`, `test_message_injection_middleware_streaming_keeps_service_conversation_id_external`, `packages/core/tests/core/test_harness_agent.py::test_streaming_harness_tool_call_does_not_duplicate_transcript` | | Service-side approval decision | Stored hosted request is skipped; the current approved or rejected hosted response is sent, while local approval controls are omitted from provider input. | `packages/openai/tests/openai/test_openai_chat_client.py::test_prepare_messages_strips_approval_request_but_keeps_response_under_storage`, `test_prepare_messages_drops_local_approval_controls` | | OpenAI approval serialization | Hosted approval id and decision serialize to `mcp_approval_response`; local approvals remain in-process. | `test_prepare_message_for_openai_with_function_approval_response`, `test_prepare_content_for_opentool_approval_response`, `test_function_approval_response_with_mcp_tool_call` | | OpenAI end-to-end hosted approval | Hosted request parses, response sends, and continuation completes. | `test_end_to_end_mcp_approval_flow` | diff --git a/python/packages/core/agent_framework/_sessions.py b/python/packages/core/agent_framework/_sessions.py index 893bb96e52..818f204533 100644 --- a/python/packages/core/agent_framework/_sessions.py +++ b/python/packages/core/agent_framework/_sessions.py @@ -1260,6 +1260,28 @@ def _response_contains_follow_up_request(response: ChatResponse) -> bool: ) +def _carry_over_stream_control_state(response: ChatResponse, inner_response: ChatResponse) -> None: + """Copy control-flow state from an inner final response onto a rebuilt outer response. + + A response rebuilt from streamed updates can only see state that was emitted on an update. + Middleware that runs closer to the leaf client (for example + :class:`PerServiceCallHistoryPersistingMiddleware`) applies its continuation state through a + result hook, i.e. on the inner final response *after* the stream has been consumed, so that + state has to be carried over explicitly. Dropping it makes the function-invocation loop treat + the turn as if no history were managed and resend the whole turn, duplicating the transcript. + + Args: + response: The outer response rebuilt from the streamed updates. + inner_response: The final response of the innermost stream that produced those updates. + """ + if inner_response.conversation_id is not None: + response.conversation_id = inner_response.conversation_id + if inner_response.has_internal_conversation_id(): + response.mark_internal_conversation_id() + else: + response.clear_internal_conversation_id() + + def _split_service_call_messages(messages: Sequence[Message]) -> tuple[list[Message], dict[str, list[Message]]]: """Split service-call messages into input messages and attributed context messages.""" input_messages: list[Message] = [] @@ -1383,6 +1405,7 @@ async def _stream_injected_messages( context: ChatContext, call_next: Callable[[], Awaitable[None]], session: AgentSession, + inner_responses: list[ChatResponse], ) -> AsyncIterable[ChatResponseUpdate]: while True: context.messages = self._drain_pending_messages(session, context.messages) @@ -1396,12 +1419,27 @@ async def _stream_injected_messages( async for update in stream: yield update response = await stream.get_final_response() + inner_responses.append(response) if _response_contains_follow_up_request(response) or not self._has_pending_messages(session): return self._update_context_conversation_id(context, response.conversation_id) empty_messages: list[Message] = [] context.messages = empty_messages + @staticmethod + def _finalize_injected_stream( + updates: Sequence[ChatResponseUpdate], + inner_responses: Sequence[ChatResponse], + response_format: Any | None, + ) -> ChatResponse: + """Rebuild the outer response from the streamed updates, keeping inner continuation state.""" + response = ChatResponse.from_updates(updates, output_format_type=response_format) + if inner_responses: + # The last inner response is the one the non-streaming path would return, so it also + # owns the continuation state for the next function-loop iteration. + _carry_over_stream_control_state(response, inner_responses[-1]) + return response + async def process(self, context: ChatContext, call_next: Callable[[], Awaitable[None]]) -> None: """Inject pending session messages into chat model calls. @@ -1424,9 +1462,10 @@ async def process(self, context: ChatContext, call_next: Callable[[], Awaitable[ return response_format = context.options.get("response_format") if context.options is not None else None + inner_responses: list[ChatResponse] = [] context.result = ResponseStream( - self._stream_injected_messages(context, call_next, session), - finalizer=lambda updates: ChatResponse.from_updates(updates, output_format_type=response_format), + self._stream_injected_messages(context, call_next, session, inner_responses), + finalizer=lambda updates: self._finalize_injected_stream(updates, inner_responses, response_format), ) diff --git a/python/packages/core/tests/core/test_harness_agent.py b/python/packages/core/tests/core/test_harness_agent.py index 36a1b5f99c..b74902116e 100644 --- a/python/packages/core/tests/core/test_harness_agent.py +++ b/python/packages/core/tests/core/test_harness_agent.py @@ -1434,3 +1434,123 @@ def test_create_harness_agent_shell_dedup_does_not_suppress_harness_warning() -> disable_file_memory=True, background_agents=[bg_agent], # type: ignore[list-item] # pyrefly: ignore[bad-argument-type] # ty: ignore[invalid-argument-type] ) + + +def _tool_call_ordering_problems(messages: Sequence[Message]) -> list[str]: + """Report assistant function calls that are not immediately followed by their results.""" + problems: list[str] = [] + index = 0 + while index < len(messages): + call_ids = [ + str(content.call_id) + for content in messages[index].contents + if content.type == "function_call" and not content.informational_only + ] + if call_ids: + result_ids: list[str] = [] + next_index = index + 1 + while next_index < len(messages) and any( + content.type == "function_result" for content in messages[next_index].contents + ): + result_ids.extend( + str(content.call_id) + for content in messages[next_index].contents + if content.type == "function_result" + ) + next_index += 1 + if sorted(call_ids) != sorted(result_ids): + problems.append(f"messages[{index}] requests {call_ids} but is followed by results {result_ids}") + index = next_index + else: + index += 1 + return problems + + +async def _run_harness_tool_call_turn( + chat_client_base: Any, + *, + stream: bool, +) -> tuple[list[list[Message]], AgentSession]: + """Run one harness turn where the model calls a tool, capturing the messages sent per model call.""" + from agent_framework import tool + + @tool + def lookup(query: str) -> str: + """Look up a fact.""" + return f"result for {query}" + + captured: list[list[Message]] = [] + + def build_updates(call_index: int) -> list[ChatResponseUpdate]: + if call_index == 0: + return [ + ChatResponseUpdate(contents=[Content.from_text("Looking it up.")], role="assistant"), + ChatResponseUpdate( + contents=[ + Content.from_function_call(call_id="call_1", name="lookup", arguments={"query": "widgets"}) + ], + role="assistant", + ), + ] + return [ChatResponseUpdate(contents=[Content.from_text("Done.")], role="assistant", finish_reason="stop")] + + def fake_streaming_response( + *, messages: Sequence[Message], options: dict[str, Any], **kwargs: Any + ) -> ResponseStream[ChatResponseUpdate, ChatResponse]: + call_index = len(captured) + captured.append(list(messages)) + + async def _stream() -> AsyncIterable[ChatResponseUpdate]: + for update in build_updates(call_index): + yield update + + return ResponseStream(_stream(), finalizer=ChatResponse.from_updates) + + async def fake_get_response(*, messages: Sequence[Message], options: dict[str, Any], **kwargs: Any) -> ChatResponse: + call_index = len(captured) + captured.append(list(messages)) + return ChatResponse.from_updates(build_updates(call_index)) + + agent = create_harness_agent( + client=chat_client_base, + tools=[lookup], + disable_web_search=True, + disable_todo=True, + disable_mode=True, + disable_file_memory=True, + ) + session = agent.create_session() + + with ( + patch.object(chat_client_base, "_get_streaming_response", side_effect=fake_streaming_response), + patch.object(chat_client_base, "_get_non_streaming_response", side_effect=fake_get_response), + ): + if stream: + async for _update in agent.run("look up widgets", session=session, stream=True): + pass + else: + await agent.run("look up widgets", session=session) + + return captured, session + + +async def test_streaming_harness_tool_call_does_not_duplicate_transcript(chat_client_base: Any) -> None: + """Regression for #7591: a streaming tool-call turn must not resend the turn twice. + + The streaming path rebuilt the response from updates in ``MessageInjectionMiddleware``, losing the + local-history conversation-id sentinel set by ``PerServiceCallHistoryPersistingMiddleware``. The + function loop then resent the whole turn on top of the injected history, leaving an assistant + function call with no results after it — which strict endpoints reject. + """ + streaming_calls, session = await _run_harness_tool_call_turn(chat_client_base, stream=True) + non_streaming_calls, _ = await _run_harness_tool_call_turn(chat_client_base, stream=False) + + assert len(streaming_calls) == 2 + second_call = streaming_calls[1] + assert _tool_call_ordering_problems(second_call) == [] + assert sum(1 for message in second_call if message.text == "look up widgets") == 1 + assert sum(1 for message in second_call for content in message.contents if content.type == "function_call") == 1 + # The streaming path must build the same transcript as the non-streaming path. + assert [message.text for message in second_call] == [message.text for message in non_streaming_calls[1]] + # The local sentinel is control-flow state and must never become a service session id. + assert session.service_session_id is None diff --git a/python/packages/core/tests/core/test_middleware_with_chat.py b/python/packages/core/tests/core/test_middleware_with_chat.py index c2b794f4c5..f1180d96ea 100644 --- a/python/packages/core/tests/core/test_middleware_with_chat.py +++ b/python/packages/core/tests/core/test_middleware_with_chat.py @@ -27,6 +27,7 @@ function_middleware, tool, ) +from agent_framework._sessions import LOCAL_HISTORY_CONVERSATION_ID from agent_framework.exceptions import ChatClientInvalidRequestException from .conftest import MockBaseChatClient @@ -624,6 +625,111 @@ async def stream() -> AsyncIterable[ChatResponseUpdate]: assert [update.text for update in updates] == ["", "done"] assert captured_messages == [["user message"], ["queued while streaming hosted tool"]] + async def test_message_injection_middleware_streaming_preserves_inner_continuation_state( + self, chat_client_base: "MockBaseChatClient" + ) -> None: + """Regression for #7591: result-hook state on the inner response survives the outer rebuild. + + ``PerServiceCallHistoryPersistingMiddleware`` marks the local-history sentinel on the inner + final response through a result hook, so it is never emitted on an update. Rebuilding the + outer response from updates dropped it, and the function loop then resent the whole turn. + """ + session = AgentSession() + injection = MessageInjectionMiddleware() + observed: list[ChatResponse] = [] + + class _ObservingMiddleware(ChatMiddleware): + """Capture the response the function-invocation loop sees for each model call.""" + + async def process(self, context: ChatContext, call_next: Callable[[], Awaitable[None]]) -> None: + await call_next() + stream = cast(ResponseStream[ChatResponseUpdate, ChatResponse], context.result) + + def record(response: ChatResponse) -> ChatResponse: + observed.append(response) + return response + + context.result = stream.with_result_hook(record) + + class _SentinelMiddleware(ChatMiddleware): + """Stand-in for the per-service-call history middleware's result hook.""" + + async def process(self, context: ChatContext, call_next: Callable[[], Awaitable[None]]) -> None: + await call_next() + stream = cast(ResponseStream[ChatResponseUpdate, ChatResponse], context.result) + + def mark(response: ChatResponse) -> ChatResponse: + response.conversation_id = LOCAL_HISTORY_CONVERSATION_ID + response.mark_internal_conversation_id() + return response + + context.result = stream.with_result_hook(mark) + + stream = chat_client_base.get_response( + [Message(role="user", contents=["user message"])], + stream=True, + client_kwargs={ + "middleware": [_ObservingMiddleware(), injection, _SentinelMiddleware()], + "session": session, + }, + ) + async for _update in stream: + pass + await stream.get_final_response() + + assert [response.conversation_id for response in observed] == [LOCAL_HISTORY_CONVERSATION_ID] + assert observed[0].has_internal_conversation_id() + + async def test_message_injection_middleware_streaming_keeps_service_conversation_id_external( + self, chat_client_base: "MockBaseChatClient" + ) -> None: + """Test that a real service conversation id is preserved and not marked internal.""" + session = AgentSession() + injection = MessageInjectionMiddleware() + observed: list[ChatResponse] = [] + + class _ObservingMiddleware(ChatMiddleware): + async def process(self, context: ChatContext, call_next: Callable[[], Awaitable[None]]) -> None: + await call_next() + stream = cast(ResponseStream[ChatResponseUpdate, ChatResponse], context.result) + + def record(response: ChatResponse) -> ChatResponse: + observed.append(response) + return response + + context.result = stream.with_result_hook(record) + + def fake_streaming_response( + *, + messages: Sequence[Message], + options: dict[str, Any], + **kwargs: Any, + ) -> ResponseStream[ChatResponseUpdate, ChatResponse]: + async def stream() -> AsyncIterable[ChatResponseUpdate]: + yield ChatResponseUpdate( + contents=[Content.from_text("done")], + role="assistant", + conversation_id="service-conversation", + ) + + return ResponseStream(stream(), finalizer=ChatResponse.from_updates) + + with patch.object(chat_client_base, "_get_streaming_response", side_effect=fake_streaming_response): + stream = chat_client_base.get_response( + [Message(role="user", contents=["user message"])], + stream=True, + client_kwargs={ + "middleware": [_ObservingMiddleware(), injection], + "session": session, + }, + ) + async for _update in stream: + pass + await stream.get_final_response() + + assert [response.conversation_id for response in observed] == ["service-conversation"] + assert not observed[0].has_internal_conversation_id() + def test_enqueue_messages_uses_session_state_queue(self) -> None: """Test that standalone message injection enqueueing stores messages in session state.""" session = AgentSession() diff --git a/python/uv.lock b/python/uv.lock index 5fb1c5a765..56b62149e5 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -159,7 +159,7 @@ dev = [ { name = "opentelemetry-sdk" }, { name = "poethepoet", specifier = "==0.48.0" }, { name = "prek", specifier = "==0.4.11" }, - { name = "pyrefly", specifier = "==1.1.1" }, + { name = "pyrefly", specifier = "==1.2.0" }, { name = "pyright", specifier = "==1.1.411" }, { name = "pytest", specifier = "==9.1.1" }, { name = "pytest-asyncio", specifier = "==1.4.0" }, @@ -172,7 +172,7 @@ dev = [ { name = "tomli", specifier = "==2.4.1" }, { name = "ty", specifier = "==0.0.64" }, { name = "uv", specifier = "==0.11.32" }, - { name = "zuban", specifier = "==0.9.0" }, + { name = "zuban", specifier = "==0.9.1" }, ] test = [ { name = "agent-hooks-sdk", specifier = ">=0.1.0a4,<0.2" }, @@ -6157,21 +6157,21 @@ wheels = [ [[package]] name = "pyrefly" -version = "1.1.1" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/20/976165fa4b1517a1a92f393b3f4d4badabfff1165eff09d4cd4908428183/pyrefly-1.1.1.tar.gz", hash = "sha256:6deda959f8603a7dbdf112c48983e2275b2903cf33c8c739ed65d7e71a4fd520", size = 5880491, upload-time = "2026-06-18T23:45:43.785Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/01/a86e9f24722b095c3f88e3616132b75a21b0df53804bdc6a45314dd4d93c/pyrefly-1.2.0.tar.gz", hash = "sha256:5485f960fc2481617068c918335c39ab1507ef90b6b5bd35bf57726e60e73185", size = 6243654, upload-time = "2026-08-01T02:56:27.592Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/d6/02ba666018c6a1cb4ddfa2db98ada721adddd374db5c29ba47a0bf2637fa/pyrefly-1.1.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f4b8595f91885bc8b5e3c282ab68d1df21201668a84e6508b1e15f2feec0bb8d", size = 13631867, upload-time = "2026-06-18T23:45:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/71/47/7a3457dbbddb513a83cf4fe527d5d5ebda5201a1010ad2a6034030e3e358/pyrefly-1.1.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6b238e1362622d47a6eb5af704fd8b613c94e8c303386efd6350e3da59fecc8", size = 13075304, upload-time = "2026-06-18T23:45:16.865Z" }, - { url = "https://files.pythonhosted.org/packages/84/df/70f4b3f42d58ed686a80df31e04eca54d88036cea4f9b96195c64ad0b2b5/pyrefly-1.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b50d4510e4f8aaea79e2c4b343a4d7a060c9451c0b2aa9bfe10d7ca1ef33d68d", size = 13446966, upload-time = "2026-06-18T23:45:19.644Z" }, - { url = "https://files.pythonhosted.org/packages/3c/53/12a19bd6c7af985bcbc13c6910d0f9f6684069ead2282a5c08c2bfbb5d03/pyrefly-1.1.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f330cf039ef3da3b910c84f3a7e431f0cf8d0c1d2dad26491d6cadf3c7cd4759", size = 14449222, upload-time = "2026-06-18T23:45:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/93/f0/e55c48a50076fc0f9ecf4bdedec50456db383e01162f5e2121f8468be071/pyrefly-1.1.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6342d87c52b04f72156da04f554c4d57f3616f2b32d1763969efb22d05a1407", size = 14472947, upload-time = "2026-06-18T23:45:24.858Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e7/30e085b31fed978ecb675bdbb54df566673ab550469e5af2d350f6af0be6/pyrefly-1.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c08b814ad03175e9cf47111390537161828b472044c39ab3320252b3ac6b2edd", size = 13975252, upload-time = "2026-06-18T23:45:27.247Z" }, - { url = "https://files.pythonhosted.org/packages/47/58/49c3e67641133d3fe5d8d9a660dc0826c6c37ca197d86cad05fa7dd8bfd6/pyrefly-1.1.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d50cad97f19fc893b04deff7239626cffff5dd27ffb29b7d303a1b770247b208", size = 13471780, upload-time = "2026-06-18T23:45:29.775Z" }, - { url = "https://files.pythonhosted.org/packages/71/1e/65a7ba8355e2c39d8331832905fb74dcc85fc122a3f1dfd6dbf2a88907ad/pyrefly-1.1.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2150b450ee6a6bcbe69b2d45d9a4ebc934a609e1abcf65e490433f38eb873d84", size = 13989306, upload-time = "2026-06-18T23:45:32.576Z" }, - { url = "https://files.pythonhosted.org/packages/37/de/b7ee1ab2392c36945738246fba7524439810befa3cfcc03cb6157567fc10/pyrefly-1.1.1-py3-none-win32.whl", hash = "sha256:5ffd8a8ed62fe4e6bf0afe1837d1bad149bb3b9f80e928ef248c96b836db3742", size = 12608469, upload-time = "2026-06-18T23:45:35.419Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9c/a0f5b52934bf80e9c7eff08222e7caf318287b9aef76acb8d9ac5740581b/pyrefly-1.1.1-py3-none-win_amd64.whl", hash = "sha256:4e0430f3ef69c8ac73505fd6584db70ed504665a9f0816fef7f723de510f26cb", size = 13502172, upload-time = "2026-06-18T23:45:38.375Z" }, - { url = "https://files.pythonhosted.org/packages/42/3d/4c6bcb3d456835f51445d3662a428f56c3ea5643ec798c577030ae34298c/pyrefly-1.1.1-py3-none-win_arm64.whl", hash = "sha256:83baf0db71e172665db1fca0ced50b8f7773f5192ca57e8ac6773a772b6d2fc5", size = 12895979, upload-time = "2026-06-18T23:45:41.026Z" }, + { url = "https://files.pythonhosted.org/packages/7d/9d/3c0ef1d4843987b22f996ed381ec9cf5a3b1273e29804db276252e4c95eb/pyrefly-1.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7f46d983ac49ddd2b043694960a01dc6a19a5cfd8eec609d6bd9c42866f91b4e", size = 14026305, upload-time = "2026-08-01T02:56:02.611Z" }, + { url = "https://files.pythonhosted.org/packages/0a/06/03bbb78fbea54cdc65b626619f3597d5611aca4fdef11e72a4e8360e7e63/pyrefly-1.2.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:756f669b5555090f5c1a4fef30db1785fabe657764f7e4e6dc88994dfb8ca82d", size = 13463880, upload-time = "2026-08-01T02:56:04.93Z" }, + { url = "https://files.pythonhosted.org/packages/13/5a/7d8bc00a38e93bbc9c3e7bd14d305f7948717e667c9bcddeab9dd42fd255/pyrefly-1.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3465812ce5ef4781fb592edbf2724547296f0a3124be115d73c7e8b2401862d", size = 13907329, upload-time = "2026-08-01T02:56:07.104Z" }, + { url = "https://files.pythonhosted.org/packages/be/94/9e08b4bf799d0b8f36b55a2783c7ba5f51730cf0632a85a67b5b5ed876cd/pyrefly-1.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5de7b2ad2bba5c8055181681a84b74143eac2234a48ba5d1b7ed7e7a722b02bd", size = 15039020, upload-time = "2026-08-01T02:56:09.208Z" }, + { url = "https://files.pythonhosted.org/packages/5b/bd/bca5fd0c80f4daf8ee6903a29df9f3de1feb05ff0946b8f35ec8c5096b13/pyrefly-1.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25822ea9505f589ea8a725e4268b475132fb89e038fbf092e446510443ac142a", size = 14986199, upload-time = "2026-08-01T02:56:11.924Z" }, + { url = "https://files.pythonhosted.org/packages/97/f7/f07087f3d185ad2eced0c56cef89ca5474dfb4ff25f146cd50a861c97553/pyrefly-1.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90efe75e17491ef5d636e10469e9278d7d0256b3b4c5e1f4750069bf3ae0f5d1", size = 14393715, upload-time = "2026-08-01T02:56:14.143Z" }, + { url = "https://files.pythonhosted.org/packages/d3/70/0d142c320e284b9e3ce35e9b1e58b8ce2ee1f578f2a7234bc30e5022b94f/pyrefly-1.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:368aaf7eee4f511ddc0f8e564cf14e01ab2f10b0db9105c6d5b153bf498d07bf", size = 13933008, upload-time = "2026-08-01T02:56:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e8/e84f11b6e1f63fd453ad3654213b9a0f6f4de8cef6b58038eef2d0d5955d/pyrefly-1.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d52d5da7bc65fb7675fbaa80eda879d4f8787c494f04cac21603330d3abbdbbe", size = 14431827, upload-time = "2026-08-01T02:56:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/0f/06/810d31380f66c75e1c0779a408d3b16117b1b368b57894f6aa66bef21686/pyrefly-1.2.0-py3-none-win32.whl", hash = "sha256:8c90751de8506d938e8f802659c74cf35bd7a0036510ee6c634a38eebb280bfa", size = 13229447, upload-time = "2026-08-01T02:56:20.921Z" }, + { url = "https://files.pythonhosted.org/packages/ed/98/4dafa3c7a1caed2dc8cc708dde09ba27963c7736508f55b626fff3024113/pyrefly-1.2.0-py3-none-win_amd64.whl", hash = "sha256:8a8964c224ccc4882730130955815de21ff443c1ac3f0b90685b19bf63848170", size = 14087387, upload-time = "2026-08-01T02:56:23.188Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1c/df3cb0a2e5591660ded7a1836cd2f29dc48c91adb1c0a3a700a96f6d09e1/pyrefly-1.2.0-py3-none-win_arm64.whl", hash = "sha256:3a90bb8df39dfbac74b1f3b2e9d7c526b8f80568884c3944d955023a73ebf61e", size = 13430873, upload-time = "2026-08-01T02:56:25.425Z" }, ] [[package]] @@ -8027,19 +8027,19 @@ wheels = [ [[package]] name = "zuban" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/4c/e656a15040892d57613797497a830ded23a1393e26790d10d1544b6c3d7f/zuban-0.9.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:88bc1de65b4c872b2be39ee8f6667bdf2445e97edff57fd36de1c8196cb9f080", size = 11490459, upload-time = "2026-06-23T08:39:19.454Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ae/effe7e2ae69b100f45bfc0fdfe791960cc5fdf0fd9f912f9dfe383831708/zuban-0.9.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6013a9e01bdd5a4806147dc305c4ccd699b5a3675a6eb753bc90c942da3e1bd2", size = 11210900, upload-time = "2026-06-23T08:39:22.361Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ef/d769640c1bb02aa63f4232e4e0800a54f83c9fecd3fb706de06c441ce221/zuban-0.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e66ceffddaa6b2c10cf7f737e2bd36c19043746b46564dfe73e9b781d1ed6ee3", size = 28430892, upload-time = "2026-06-23T08:39:24.785Z" }, - { url = "https://files.pythonhosted.org/packages/33/5a/794e266304476f2f1270f1c65eb3802905e5c7a3ec4cb9e2f80c43ddabbd/zuban-0.9.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eac519c610f19473f9cd307e1601e935567aac19b0732a50826590303d3f811c", size = 28625883, upload-time = "2026-06-23T08:39:27.592Z" }, - { url = "https://files.pythonhosted.org/packages/4a/83/a0c95efca0f59fb0a694bdffcc5926adac99e218d38bc2791110655055ef/zuban-0.9.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95d1620ad4d88c00cdcbcbd623c50647af5d7e67a5cf6270f6c7e0bc27acb2bc", size = 29832269, upload-time = "2026-06-23T08:39:30.195Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f3/170ac2bb7dfa94651d415d6ee791fe2bfdc65a7adc8dfe4ac1c9496c82b1/zuban-0.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65bbde3810865595756c25447dd854991f910486253dfbcad7518314c501c8fd", size = 30901959, upload-time = "2026-06-23T08:39:35.136Z" }, - { url = "https://files.pythonhosted.org/packages/d3/75/3234eecd716e2808b65c29ccfe42191433d9d8359dc793f88a1dbb0d3ffa/zuban-0.9.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bca1d0be28048ec6c9b4873461cba83c5250046814ba0079f6a7f89a47433cd1", size = 28593375, upload-time = "2026-06-23T08:39:38.39Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b0/64de2f8ef6cd265b55e9237d131907d659d6c1af0d4b2656cdd078713013/zuban-0.9.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bb6c3bf9a28768e6c4c2e47359439b818b21d84fc644ec46b4f3f2e165824506", size = 29034957, upload-time = "2026-06-23T08:39:41.221Z" }, - { url = "https://files.pythonhosted.org/packages/ff/3d/5b97101e94f71a35acf79cf17470e57679ed2adbf7acb0d4ef1869fc35bf/zuban-0.9.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1617ebcb962f18c5c63a80620dd1540593ae33932c244beafd9f9d050adbabbb", size = 29710311, upload-time = "2026-06-23T08:39:45.1Z" }, - { url = "https://files.pythonhosted.org/packages/5f/43/79fa5f9c1f4fc99d27d59f1409e1503b885c0dd8fd701c0c3766dea5d140/zuban-0.9.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8ba529a06f090e19ce5f87174f617435a3669ef37e5ead17970117d9cb43bd82", size = 29075890, upload-time = "2026-06-23T08:39:47.983Z" }, - { url = "https://files.pythonhosted.org/packages/7b/b9/b11c0b83bb4f544b53ac2b62b766e73537dbea89774c3aef0a65f18eb378/zuban-0.9.0-py3-none-win32.whl", hash = "sha256:cf4b1d71da43a1efdb29863c9b23d3855658f00f8f86ad7932504803d93a8072", size = 10044779, upload-time = "2026-06-23T08:39:50.906Z" }, - { url = "https://files.pythonhosted.org/packages/25/73/ebc3a4cfc08216cde168e968ad7f8289c94c8ede78adf18dd15b52d855ab/zuban-0.9.0-py3-none-win_amd64.whl", hash = "sha256:4889a911b72269258c54c94a250450ee721e6c7c6ad058c416286e83fa3f3685", size = 10806625, upload-time = "2026-06-23T08:39:53.433Z" }, +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/cf/7701477eab6244532447ff6132ea2af2c7aec2f1ef82db446d018aee7ad8/zuban-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bf9d76d87215ac06433016353c7a751d0bb570f5bd7065ee884003a7333e6d52", size = 11346895, upload-time = "2026-07-31T22:12:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/70/d1/6db81a0e25b59431313d08c8ba7612708fa6134a7ef881f438b777ad25cf/zuban-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:710eeec6725dc55a268f86b09d9a72e2ca0a9852800d0a58a20124732426eedb", size = 11073232, upload-time = "2026-07-31T22:12:48.968Z" }, + { url = "https://files.pythonhosted.org/packages/1b/36/c7f7bb36d634387c9f7dddd0201760a8755b2e65b80697cf207ee62ff741/zuban-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d10b28ed050f50b1e0cfe2a6d236d18361376a822dd0d28f9ddd4768733ee5d", size = 28278516, upload-time = "2026-07-31T22:12:51.604Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ff/32a9a8bd4c33a22abd3f7b2a1d2b21643627608ca51a59503268dd390646/zuban-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab65c8bd0bb89f4cd5be56ebf8c63223b68f8d125f6883b39c304c2ef245a5cf", size = 28571449, upload-time = "2026-07-31T22:12:54.905Z" }, + { url = "https://files.pythonhosted.org/packages/9d/30/9112460b7c069338b6f1262e4663b744a149ea58520022e867cb19f5c014/zuban-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7832d97411a005880f7e89588dc8a207cc2e17a3d3b7875aadf7e1553ae1f4a6", size = 29739155, upload-time = "2026-07-31T22:12:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f9/5ab411dbcfc118934feb19d2a80dba79277d4be2ebf89554e97918c2a826/zuban-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabf684202197630b4ba23eb236e037ed2627bbb4acdb34db8c7288e9668ad5d", size = 31614638, upload-time = "2026-07-31T22:13:01.03Z" }, + { url = "https://files.pythonhosted.org/packages/4b/67/bbbb52fc7bbb773bdfa4cf546f9c15136826c9c4a03beddb3686db2c7f7e/zuban-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67354f17d0e267c633dc8ae287c6c062d9b8f5f1ca83cc7cca85de7f0d88e686", size = 28455302, upload-time = "2026-07-31T22:13:03.819Z" }, + { url = "https://files.pythonhosted.org/packages/e9/c8/b57ac05d879c54e69947cd270e0da21fd76dda6cece135e528f2c459f9bf/zuban-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8a828388987d0d0e77160e9ae8e9fd1b6f65d071812b0004f6537c1a9d694a07", size = 28981446, upload-time = "2026-07-31T22:13:07.201Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/2f6cfaa2a319e1c5b06c62ee2710c55416cc660a3fff83ab354654d5d721/zuban-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:25dee4561c8e8c9bb5da98c13e7ea5a1d9bbf2778f7c0f7251599f7051ef52ec", size = 29610910, upload-time = "2026-07-31T22:13:10.209Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/36b5187c5ac5b80516e6e0cdf240282a8c49e7ee8ae88a426d0b0328963c/zuban-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:387407431d876c0b9993c14e3aa2b2ccd19e443b1a7dde72a5f7f55e864118f1", size = 28894561, upload-time = "2026-07-31T22:13:12.991Z" }, + { url = "https://files.pythonhosted.org/packages/62/f1/5a27e21f534fb2349fa914aa688d431579e5f49b0841f958618821660a6d/zuban-0.9.1-py3-none-win32.whl", hash = "sha256:ccafab33ae98e0ae9a826010d954f367a4a8c76378c58c5bc75bdedceaa54e70", size = 10044309, upload-time = "2026-07-31T22:13:15.558Z" }, + { url = "https://files.pythonhosted.org/packages/b9/32/ca6d67180dcbc408c0ec6dd55915dd8471982dde4443dcd492d7d52610ec/zuban-0.9.1-py3-none-win_amd64.whl", hash = "sha256:c401b88742e8a501c68f4ec605d7ebc6a63401653c15f0173a76febe161bd53e", size = 10724603, upload-time = "2026-07-31T22:13:18.086Z" }, ]