mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-02 14:28:46 +02:00
Bumps the minor-and-patch group with 7 updates in the /libs/sdk-py directory: | Package | From | To | | --- | --- | --- | | [orjson](https://github.com/ijl/orjson) | `3.11.9` | `3.12.0` | | [langchain-protocol](https://github.com/langchain-ai/agent-protocol) | `0.0.18` | `0.0.19` | | [langchain-core](https://github.com/langchain-ai/langchain) | `1.5.3` | `1.6.1` | | [ruff](https://github.com/astral-sh/ruff) | `0.16.1` | `0.16.5` | | [ty](https://github.com/astral-sh/ty) | `0.0.66` | `0.0.75` | | [starlette](https://github.com/Kludex/starlette) | `1.3.1` | `1.6.0` | | [pydantic](https://github.com/pydantic/pydantic) | `2.13.4` | `2.13.5` | Updates `orjson` from 3.11.9 to 3.12.0 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/ijl/orjson/releases">orjson's releases</a>.</em></p> <blockquote> <h2>3.12.0</h2> <h3>Changed</h3> <ul> <li>Serialization implementation substantially rewritten.</li> <li>Publish PyPI wheels for Python 3.15. For Python 3.15 and later, <code>manylinux_2_39</code> (2024) is targeted instead of <code>manylinux_2_17</code> (2012).</li> <li>No longer publish PyPI wheels for ppc64le and s390x.</li> </ul> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/ijl/orjson/blob/master/CHANGELOG.md">orjson's changelog</a>.</em></p> <blockquote> <h2>3.12.0 - 2026-08-14</h2> <h3>Changed</h3> <ul> <li>Serialization implementation substantially rewritten.</li> <li>Publish PyPI wheels for Python 3.15. For Python 3.15 and later, <code>manylinux_2_39</code> (2024) is targeted instead of <code>manylinux_2_17</code> (2012).</li> <li>No longer publish PyPI wheels for ppc64le and s390x.</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/ijl/orjson/commit/6737895a1a4e3e26df0569a40147893a786f9a58"><code>6737895</code></a> 3.12.0</li> <li><a href="https://github.com/ijl/orjson/commit/c2a6e8ff7b898635fb68a85bd5a62df1edf61cfc"><code>c2a6e8f</code></a> JsonWriter, iterators</li> <li><a href="https://github.com/ijl/orjson/commit/6a2d7a7d66dc3c5698625a7b334c40db459e46ae"><code>6a2d7a7</code></a> yyjson 1ea2fb0</li> <li><a href="https://github.com/ijl/orjson/commit/97bf170d9726e91c891f35eae4892801520b9dce"><code>97bf170</code></a> build update</li> <li>See full diff in <a href="https://github.com/ijl/orjson/compare/3.11.9...3.12.0">compare view</a></li> </ul> </details> <br /> Updates `langchain-protocol` from 0.0.18 to 0.0.19 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/langchain-ai/agent-protocol/releases">langchain-protocol's releases</a>.</em></p> <blockquote> <h2>langchain-protocol==0.0.19</h2> <h2>What's Changed</h2> <ul> <li>fix(tooling): resolve datamodel-code-generator alerts by <a href="https://github.com/jkennedyvz"><code>@jkennedyvz</code></a> in <a href="https://redirect.github.com/langchain-ai/agent-protocol/pull/93">langchain-ai/agent-protocol#93</a></li> <li>feat: add LangSmith routing to run start by <a href="https://github.com/mdrxy"><code>@mdrxy</code></a> in <a href="https://redirect.github.com/langchain-ai/agent-protocol/pull/95">langchain-ai/agent-protocol#95</a></li> <li>release: v0.19.0 by <a href="https://github.com/christian-bromann"><code>@christian-bromann</code></a> in <a href="https://redirect.github.com/langchain-ai/agent-protocol/pull/96">langchain-ai/agent-protocol#96</a></li> <li>fix(py): pin hatchling to avoid metadata 2.5 publish failure by <a href="https://github.com/christian-bromann"><code>@christian-bromann</code></a> in <a href="https://redirect.github.com/langchain-ai/agent-protocol/pull/97">langchain-ai/agent-protocol#97</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/langchain-ai/agent-protocol/compare/langchain-protocol==0.0.18...langchain-protocol==0.0.19">https://github.com/langchain-ai/agent-protocol/compare/langchain-protocol==0.0.18...langchain-protocol==0.0.19</a></p> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/langchain-ai/agent-protocol/commit/59c02be488eb7adb98ae3450983fff293294e67a"><code>59c02be</code></a> fix(py): pin hatchling to avoid metadata 2.5 publish failure (<a href="https://redirect.github.com/langchain-ai/agent-protocol/issues/97">#97</a>)</li> <li><a href="https://github.com/langchain-ai/agent-protocol/commit/c59077b743fd3dd0b86f048a2b59128de413663d"><code>c59077b</code></a> release: v0.19.0 (<a href="https://redirect.github.com/langchain-ai/agent-protocol/issues/96">#96</a>)</li> <li><a href="https://github.com/langchain-ai/agent-protocol/commit/870ef9560fab320236d6e24770e8950c5a9be63d"><code>870ef95</code></a> Merge pull request <a href="https://redirect.github.com/langchain-ai/agent-protocol/issues/95">#95</a> from langchain-ai/open-swe/v3-replica-routing</li> <li><a href="https://github.com/langchain-ai/agent-protocol/commit/175a1b8f2ac974e9b5097de90a51f1bc7ade73e0"><code>175a1b8</code></a> feat(protocol): add LangSmith routing to run start</li> <li><a href="https://github.com/langchain-ai/agent-protocol/commit/d9fd23d762bf4cffe876890a854fb79bcdd14f1a"><code>d9fd23d</code></a> Merge pull request <a href="https://redirect.github.com/langchain-ai/agent-protocol/issues/93">#93</a> from langchain-ai/langster/fix-datamodel-code-generato...</li> <li><a href="https://github.com/langchain-ai/agent-protocol/commit/9476948ab0ce1027131b58b9813f6bd3ba238ef8"><code>9476948</code></a> fix(tooling): update code generator dependencies</li> <li>See full diff in <a href="https://github.com/langchain-ai/agent-protocol/compare/langchain-protocol==0.0.18...langchain-protocol==0.0.19">compare view</a></li> </ul> </details> <br /> Updates `langchain-core` from 1.5.3 to 1.6.1 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/langchain-ai/langchain/releases">langchain-core's releases</a>.</em></p> <blockquote> <h2>langchain-core==1.6.1</h2> <p>Changes since langchain-core==1.6.0</p> <p>revert: release(core): 1.6.2 (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39971">#39971</a>) release(core): 1.6.2 (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39967">#39967</a>) fix(core): shore up indexing in genai v1 streaming content (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39964">#39964</a>) fix(core): make <code>StructuredTool</code> JSON-serializable (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39631">#39631</a>) chore(deps): bump minor and patch dependencies (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39869">#39869</a>) release(core): 1.6.1 (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39832">#39832</a>) feat(core): propagate gateway information on error path (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39829">#39829</a>)</p> <h2>langchain-core==1.6.0</h2> <p>Changes since langchain-core==1.5.6</p> <p>release(core): 1.6.0 (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39760">#39760</a>) fix(core): resolve postponed annotations in <code>StructuredTool._injected_args_keys</code> (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39602">#39602</a>) feat(core): add standard model exception types (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39538">#39538</a>) fix(core): allow deserializing <code>RunnablePick</code> (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39753">#39753</a>) fix(core): make <code>convert_to_openai_function</code> handle callables and non-dict mappings (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39750">#39750</a>) fix(core): make subprocess and temporary file tests portable on Windows (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39664">#39664</a>) fix(core): fail fast when tool schemas can't resolve forward refs during serialization (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39570">#39570</a>) test(core): avoid version-dependent runnable snapshots (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39705">#39705</a>) fix(core): require all nested properties for strict tool schemas (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39306">#39306</a>) fix(core): remove stale sync-stream <code>xfail</code> [closes <a href="https://redirect.github.com/langchain-ai/langchain/issues/39720">#39720</a>] (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39723">#39723</a>) perf(core): Lazily import transformers (<a href="https://redirect.github.com/langchain-ai/langchain/issues/38037">#38037</a>) fix(core): accept non-dict Mapping values in mustache templates (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39680">#39680</a>) docs(core): clarify <code>Runnable</code> pipe coercion [closes <a href="https://redirect.github.com/langchain-ai/langchain/issues/39075">#39075</a>] (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39707">#39707</a>) fix(core): finalize chain-group runs on <code>BaseException</code> (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39699">#39699</a>)</p> <h2>langchain-core==1.5.6</h2> <p>Changes since langchain-core==1.5.5</p> <p>chore(core): release 1.5.6 (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39704">#39704</a>) feat(core): incorporate gateway metadata to traces (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39703">#39703</a>)</p> <h2>langchain-core==1.5.5</h2> <p>Changes since langchain-core==1.5.4</p> <p>release(core): 1.5.5 (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39655">#39655</a>) fix(core): make abatch_iterate consistent with batch_iterate for None and zero size (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39367">#39367</a>) fix(core): respect pydantic aliases when validating tool inputs (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39572">#39572</a>) fix(core): issues in merging chunks (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39535">#39535</a>) fix(core): handle v1 base model validation in async path (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39576">#39576</a>) fix(core): handle tool descriptions for infer_schema=False (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39573">#39573</a>) fix(core): clear usage metadata callback on exceptions in context manager (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39616">#39616</a>) fix(core): handle falsy LLM and chat model caches (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39283">#39283</a>) chore(core): add httpx as an explicit dep (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39612">#39612</a>) fix(core): preserve non-str/non-dict items in <code>DictPromptTemplate</code> list values (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39588">#39588</a>) fix(core): raise ValueError when explicit tool_outputs length mismatches tool_calls in tool_example_to_messages (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39142">#39142</a>) fix(core): guard malformed Anthropic content blocks (<a href="https://redirect.github.com/langchain-ai/langchain/issues/38670">#38670</a>)</p> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/langchain-ai/langchain/commit/4fe9d3062f4b68e2e472eb92decf369c93aebb46"><code>4fe9d30</code></a> chore(openai): fix tests (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39972">#39972</a>)</li> <li><a href="https://github.com/langchain-ai/langchain/commit/8fa38dc143faecec0287395bce4484af4b82254c"><code>8fa38dc</code></a> revert: release(core): 1.6.2 (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39971">#39971</a>)</li> <li><a href="https://github.com/langchain-ai/langchain/commit/122030d79e944a5b55c33847d29bcfbe64488372"><code>122030d</code></a> release(core): 1.6.2 (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39967">#39967</a>)</li> <li><a href="https://github.com/langchain-ai/langchain/commit/7d4b42b57235020e6f496fdfebab44c3ca1b1f5b"><code>7d4b42b</code></a> release(langchain): 1.3.18 (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39966">#39966</a>)</li> <li><a href="https://github.com/langchain-ai/langchain/commit/4a66e355da07b8fad7d6cb8db99287fb4d7e6d83"><code>4a66e35</code></a> fix(langchain): preserve content-block shape in PIIMiddleware redaction (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39894">#39894</a>)</li> <li><a href="https://github.com/langchain-ai/langchain/commit/38e211359f0b65b480d9ee4c53f2385f8c249bb3"><code>38e2113</code></a> fix(core): shore up indexing in genai v1 streaming content (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39964">#39964</a>)</li> <li><a href="https://github.com/langchain-ai/langchain/commit/13b1b2feae476fdcebc0a285a723d5cbfed9df2e"><code>13b1b2f</code></a> fix(core): make <code>StructuredTool</code> JSON-serializable (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39631">#39631</a>)</li> <li><a href="https://github.com/langchain-ai/langchain/commit/3b3b308e6d956c9a670e41e66a468466fe6f8c26"><code>3b3b308</code></a> release(anthropic): 1.7.0 (<a href="https://redirect.github.com/langchain-ai/langchain/issues/39963">#39963</a>)</li> <li><a href="https://github.com/langchain-ai/langchain/commit/e3f6adfe1976a773704a20fef7bb03ac0309e0e4"><code>e3f6adf</code></a> feat(anthropic): support top-level param for skills via <code>container</code>; `updates...</li> <li><a href="https://github.com/langchain-ai/langchain/commit/c253f54b905f9648c50b723f01945f39a256d8aa"><code>c253f54</code></a> fix(openai): correct <code>reasoning_effort_levels</code> for gpt-5 and gpt-5.1 profiles...</li> <li>Additional commits viewable in <a href="https://github.com/langchain-ai/langchain/compare/langchain-core==1.5.3...langchain-core==1.6.1">compare view</a></li> </ul> </details> <br /> Updates `ruff` from 0.16.1 to 0.16.5 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/astral-sh/ruff/releases">ruff's releases</a>.</em></p> <blockquote> <h2>0.16.5</h2> <h2>Release Notes</h2> <p>Released on 2026-08-27.</p> <h3>Preview features</h3> <ul> <li>Allow rules without codes (<a href="https://redirect.github.com/astral-sh/ruff/pull/28049">#28049</a>)</li> <li>Introduce category selectors (<a href="https://redirect.github.com/astral-sh/ruff/pull/27666">#27666</a>)</li> <li>Update preview default rules and categories (<a href="https://redirect.github.com/astral-sh/ruff/pull/27877">#27877</a>)</li> </ul> <h3>Bug fixes</h3> <ul> <li>[<code>flake8-async</code>] Detect blocking generic HTTP requests (<code>ASYNC210</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/28024">#28024</a>)</li> <li>[<code>flake8-datetimez</code>] Allow timezone-safe <code>strptime</code> chains (<code>DTZ007</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/28023">#28023</a>)</li> <li>[<code>flake8-simplify</code>] Respect side effects in <code>lambda</code> defaults (<code>SIM401</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/28000">#28000</a>)</li> </ul> <h3>Server</h3> <ul> <li>Fix duplicated "of" in <code>ClientOptions</code> doc comment (<a href="https://redirect.github.com/astral-sh/ruff/pull/27978">#27978</a>)</li> </ul> <h3>Documentation</h3> <ul> <li>Document rule acceptance guidelines (<a href="https://redirect.github.com/astral-sh/ruff/pull/27910">#27910</a>)</li> <li>Document the new category selectors (<a href="https://redirect.github.com/astral-sh/ruff/pull/27906">#27906</a>)</li> </ul> <h3>Contributors</h3> <ul> <li><a href="https://github.com/AlexWaygood"><code>@AlexWaygood</code></a></li> <li><a href="https://github.com/sharkdp"><code>@sharkdp</code></a></li> <li><a href="https://github.com/jelle-openai"><code>@jelle-openai</code></a></li> <li><a href="https://github.com/charliermarsh"><code>@charliermarsh</code></a></li> <li><a href="https://github.com/ntBre"><code>@ntBre</code></a></li> <li><a href="https://github.com/aarushkandukoori"><code>@aarushkandukoori</code></a></li> </ul> <h2>Install ruff 0.16.5</h2> <h3>Install prebuilt binaries via shell script</h3> <pre lang="sh"><code>curl --proto '=https' --tlsv1.2 -LsSf https://releases.astral.sh/github/ruff/releases/download/0.16.5/ruff-installer.sh | sh </code></pre> <h3>Install prebuilt binaries via powershell script</h3> <pre lang="sh"><code>powershell -ExecutionPolicy Bypass -c "irm https://releases.astral.sh/github/ruff/releases/download/0.16.5/ruff-installer.ps1 | iex" </code></pre> <h2>Download ruff 0.16.5</h2> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md">ruff's changelog</a>.</em></p> <blockquote> <h2>0.16.5</h2> <p>Released on 2026-08-27.</p> <h3>Preview features</h3> <ul> <li>Allow rules without codes (<a href="https://redirect.github.com/astral-sh/ruff/pull/28049">#28049</a>)</li> <li>Introduce category selectors (<a href="https://redirect.github.com/astral-sh/ruff/pull/27666">#27666</a>)</li> <li>Update preview default rules and categories (<a href="https://redirect.github.com/astral-sh/ruff/pull/27877">#27877</a>)</li> </ul> <h3>Bug fixes</h3> <ul> <li>[<code>flake8-async</code>] Detect blocking generic HTTP requests (<code>ASYNC210</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/28024">#28024</a>)</li> <li>[<code>flake8-datetimez</code>] Allow timezone-safe <code>strptime</code> chains (<code>DTZ007</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/28023">#28023</a>)</li> <li>[<code>flake8-simplify</code>] Respect side effects in <code>lambda</code> defaults (<code>SIM401</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/28000">#28000</a>)</li> </ul> <h3>Server</h3> <ul> <li>Fix duplicated "of" in <code>ClientOptions</code> doc comment (<a href="https://redirect.github.com/astral-sh/ruff/pull/27978">#27978</a>)</li> </ul> <h3>Documentation</h3> <ul> <li>Document rule acceptance guidelines (<a href="https://redirect.github.com/astral-sh/ruff/pull/27910">#27910</a>)</li> <li>Document the new category selectors (<a href="https://redirect.github.com/astral-sh/ruff/pull/27906">#27906</a>)</li> </ul> <h3>Contributors</h3> <ul> <li><a href="https://github.com/AlexWaygood"><code>@AlexWaygood</code></a></li> <li><a href="https://github.com/sharkdp"><code>@sharkdp</code></a></li> <li><a href="https://github.com/jelle-openai"><code>@jelle-openai</code></a></li> <li><a href="https://github.com/charliermarsh"><code>@charliermarsh</code></a></li> <li><a href="https://github.com/ntBre"><code>@ntBre</code></a></li> <li><a href="https://github.com/aarushkandukoori"><code>@aarushkandukoori</code></a></li> </ul> <h2>0.16.4</h2> <p>Released on 2026-08-20.</p> <h3>Preview features</h3> <ul> <li>[<code>flake8-use-pathlib</code>] Add autofix for <code>PTH116</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/26460">#26460</a>)</li> <li>[<code>refurb</code>] Restrict <code>delete-full-slice</code> to lists (<code>FURB131</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27711">#27711</a>)</li> <li>[<code>refurb</code>] Skip <code>FURB101</code> and <code>FURB103</code> when the <code>open</code> argument is a file descriptor (<a href="https://redirect.github.com/astral-sh/ruff/pull/27643">#27643</a>)</li> </ul> <h3>Bug fixes</h3> <ul> <li>Fix <code>InvalidInstruction</code> on Windows CPUs that do not support <code>POPCNT</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/27803">#27803</a>)</li> <li>[<code>pyflakes</code>] Emit semantic syntax errors in string type definitions as <code>F722</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/27835">#27835</a>)</li> <li>[<code>pylint</code>] Allow <code>os._exit</code> imports in <code>import-private-name</code> (<code>PLC2701</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27738">#27738</a>)</li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/astral-sh/ruff/commit/9e4938c4a60bed3e87a11ee1e1db1bd23f4d964a"><code>9e4938c</code></a> Bump 0.16.5 (<a href="https://redirect.github.com/astral-sh/ruff/issues/28110">#28110</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/aad0e909ef1390f4b2a3ba8aa0a67fb8ea5cbacd"><code>aad0e90</code></a> Allow rules without codes (<a href="https://redirect.github.com/astral-sh/ruff/issues/28049">#28049</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/5fdab73c5052350400c36b08c5d7710210343bc4"><code>5fdab73</code></a> Update preview default rules and categories (<a href="https://redirect.github.com/astral-sh/ruff/issues/27877">#27877</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/29c8e5b2d0a46eb7dc4ff11c1b0a0dc5ccea52e4"><code>29c8e5b</code></a> Document rule acceptance guidelines (<a href="https://redirect.github.com/astral-sh/ruff/issues/27910">#27910</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/50a4d7fd106603a5616b01ac3bef3306252b248f"><code>50a4d7f</code></a> Document the new category selectors (<a href="https://redirect.github.com/astral-sh/ruff/issues/27906">#27906</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/ada87950ea188f882f69b7bd6e2213a9696e3ee2"><code>ada8795</code></a> Introduce category selectors (<a href="https://redirect.github.com/astral-sh/ruff/issues/27666">#27666</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/d8947238863b61922bfc83f07edcc697c1cc07c0"><code>d894723</code></a> [ty] Infer lambda parameters through callable type aliases (<a href="https://redirect.github.com/astral-sh/ruff/issues/28109">#28109</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/2685fdebbcf9938736fed8c45886a629f9c99a06"><code>2685fde</code></a> [ty] Narrow functional enum members in <code>==</code> and <code>match</code> (<a href="https://redirect.github.com/astral-sh/ruff/issues/28103">#28103</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/efcffd2178ce62e9951a53d4c50cadc225a0cfec"><code>efcffd2</code></a> [ty] Intersection simplifications with subtype-related generic specialization...</li> <li><a href="https://github.com/astral-sh/ruff/commit/eb780488037504e11f145ed778654fd8a825028b"><code>eb78048</code></a> [ty] Bump ecosystem-analyzer for HTML escaping (<a href="https://redirect.github.com/astral-sh/ruff/issues/28104">#28104</a>)</li> <li>Additional commits viewable in <a href="https://github.com/astral-sh/ruff/compare/0.16.1...0.16.5">compare view</a></li> </ul> </details> <br /> Updates `ty` from 0.0.66 to 0.0.75 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/astral-sh/ty/releases">ty's releases</a>.</em></p> <blockquote> <h2>0.0.75</h2> <h2>Release Notes</h2> <p>Released on 2026-08-26.</p> <h3>Preview features</h3> <ul> <li>Initialize PEP 723 script environments in the CLI (<a href="https://redirect.github.com/astral-sh/ruff/pull/27544">#27544</a>)</li> <li>Refresh PEP 723 script environments in watch mode (<a href="https://redirect.github.com/astral-sh/ruff/pull/27617">#27617</a>)</li> <li>Run PEP 723 script synchronization on bounded workers (<a href="https://redirect.github.com/astral-sh/ruff/pull/27615">#27615</a>)</li> </ul> <h3>Bug fixes</h3> <ul> <li>Specialize <code>Self</code> bounds of inherited methods (<a href="https://redirect.github.com/astral-sh/ruff/pull/27990">#27990</a>)</li> </ul> <h3>LSP server</h3> <ul> <li>Add "Go to Definition" support for pytest fixtures (<a href="https://redirect.github.com/astral-sh/ruff/pull/27444">#27444</a>)</li> </ul> <h3>Documentation</h3> <ul> <li>Fix documented Python package build command (<a href="https://redirect.github.com/astral-sh/ty/pull/4384">#4384</a>)</li> <li>Link untyped-call tracking issue in migration guide (<a href="https://redirect.github.com/astral-sh/ty/pull/4382">#4382</a>)</li> </ul> <h3>Library support</h3> <ul> <li>Resolve imported pytest fixture exposures (<a href="https://redirect.github.com/astral-sh/ruff/pull/27539">#27539</a>)</li> <li>Resolve installed core pytest fixture providers (<a href="https://redirect.github.com/astral-sh/ruff/pull/27770">#27770</a>)</li> <li>Resolve pytest fixtures through conftest (<a href="https://redirect.github.com/astral-sh/ruff/pull/27540">#27540</a>)</li> </ul> <h3>Diagnostics</h3> <ul> <li>Add more autofixes (<a href="https://redirect.github.com/astral-sh/ruff/pull/28029">#28029</a>)</li> <li>Add a dedicated missing-slot diagnostic (<a href="https://redirect.github.com/astral-sh/ruff/pull/28039">#28039</a>)</li> <li>Explain missing storage for declared slotted attributes (<a href="https://redirect.github.com/astral-sh/ruff/pull/27969">#27969</a>)</li> <li>Improve diagnostic spans for unpacked variable assignments (<a href="https://redirect.github.com/astral-sh/ruff/pull/28041">#28041</a>)</li> </ul> <h3>Core type checking</h3> <ul> <li>Account for known subclasses in equality inference (<a href="https://redirect.github.com/astral-sh/ruff/pull/28005">#28005</a>)</li> <li>Expand <code>ParamSpec</code> signatures inferred from bound receivers (<a href="https://redirect.github.com/astral-sh/ruff/pull/28020">#28020</a>)</li> <li>Fix <code>Self</code> binding in <code>ParamSpec</code> protocols (<a href="https://redirect.github.com/astral-sh/ruff/pull/28016">#28016</a>)</li> <li>Fix <code>TypedDict</code> variance inference (<a href="https://redirect.github.com/astral-sh/ruff/pull/28052">#28052</a>)</li> <li>Fix unsound narrowing through branch-assigned conditions (<a href="https://redirect.github.com/astral-sh/ruff/pull/28006">#28006</a>)</li> <li>Ignore inconsistent binding decorators on overloads (<a href="https://redirect.github.com/astral-sh/ruff/pull/28036">#28036</a>)</li> <li>Infer <code>yield from</code> send/return types from the iterator returned by <code>__iter__</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/27987">#27987</a>)</li> <li>Infer tuple type parameters from union arguments (<a href="https://redirect.github.com/astral-sh/ruff/pull/28062">#28062</a>)</li> <li>Infer variance through nonrecursive protocol references (<a href="https://redirect.github.com/astral-sh/ruff/pull/28065">#28065</a>)</li> <li>Preserve bounds of non-literal metaclasses (<a href="https://redirect.github.com/astral-sh/ruff/pull/28046">#28046</a>)</li> <li>Preserve correlated generic-call inference (<a href="https://redirect.github.com/astral-sh/ruff/pull/28043">#28043</a>)</li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/astral-sh/ty/blob/main/CHANGELOG.md">ty's changelog</a>.</em></p> <blockquote> <h2>0.0.75</h2> <p>Released on 2026-08-26.</p> <h3>Preview features</h3> <ul> <li>Initialize PEP 723 script environments in the CLI (<a href="https://redirect.github.com/astral-sh/ruff/pull/27544">#27544</a>)</li> <li>Refresh PEP 723 script environments in watch mode (<a href="https://redirect.github.com/astral-sh/ruff/pull/27617">#27617</a>)</li> <li>Run PEP 723 script synchronization on bounded workers (<a href="https://redirect.github.com/astral-sh/ruff/pull/27615">#27615</a>)</li> </ul> <h3>Bug fixes</h3> <ul> <li>Specialize <code>Self</code> bounds of inherited methods (<a href="https://redirect.github.com/astral-sh/ruff/pull/27990">#27990</a>)</li> </ul> <h3>LSP server</h3> <ul> <li>Add "Go to Definition" support for pytest fixtures (<a href="https://redirect.github.com/astral-sh/ruff/pull/27444">#27444</a>)</li> </ul> <h3>Documentation</h3> <ul> <li>Fix documented Python package build command (<a href="https://redirect.github.com/astral-sh/ty/pull/4384">#4384</a>)</li> <li>Link untyped-call tracking issue in migration guide (<a href="https://redirect.github.com/astral-sh/ty/pull/4382">#4382</a>)</li> </ul> <h3>Library support</h3> <ul> <li>Resolve imported pytest fixture exposures (<a href="https://redirect.github.com/astral-sh/ruff/pull/27539">#27539</a>)</li> <li>Resolve installed core pytest fixture providers (<a href="https://redirect.github.com/astral-sh/ruff/pull/27770">#27770</a>)</li> <li>Resolve pytest fixtures through conftest (<a href="https://redirect.github.com/astral-sh/ruff/pull/27540">#27540</a>)</li> </ul> <h3>Diagnostics</h3> <ul> <li>Add more autofixes (<a href="https://redirect.github.com/astral-sh/ruff/pull/28029">#28029</a>)</li> <li>Add a dedicated missing-slot diagnostic (<a href="https://redirect.github.com/astral-sh/ruff/pull/28039">#28039</a>)</li> <li>Explain missing storage for declared slotted attributes (<a href="https://redirect.github.com/astral-sh/ruff/pull/27969">#27969</a>)</li> <li>Improve diagnostic spans for unpacked variable assignments (<a href="https://redirect.github.com/astral-sh/ruff/pull/28041">#28041</a>)</li> </ul> <h3>Core type checking</h3> <ul> <li>Account for known subclasses in equality inference (<a href="https://redirect.github.com/astral-sh/ruff/pull/28005">#28005</a>)</li> <li>Expand <code>ParamSpec</code> signatures inferred from bound receivers (<a href="https://redirect.github.com/astral-sh/ruff/pull/28020">#28020</a>)</li> <li>Fix <code>Self</code> binding in <code>ParamSpec</code> protocols (<a href="https://redirect.github.com/astral-sh/ruff/pull/28016">#28016</a>)</li> <li>Fix <code>TypedDict</code> variance inference (<a href="https://redirect.github.com/astral-sh/ruff/pull/28052">#28052</a>)</li> <li>Fix unsound narrowing through branch-assigned conditions (<a href="https://redirect.github.com/astral-sh/ruff/pull/28006">#28006</a>)</li> <li>Ignore inconsistent binding decorators on overloads (<a href="https://redirect.github.com/astral-sh/ruff/pull/28036">#28036</a>)</li> <li>Infer <code>yield from</code> send/return types from the iterator returned by <code>__iter__</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/27987">#27987</a>)</li> <li>Infer tuple type parameters from union arguments (<a href="https://redirect.github.com/astral-sh/ruff/pull/28062">#28062</a>)</li> <li>Infer variance through nonrecursive protocol references (<a href="https://redirect.github.com/astral-sh/ruff/pull/28065">#28065</a>)</li> <li>Preserve bounds of non-literal metaclasses (<a href="https://redirect.github.com/astral-sh/ruff/pull/28046">#28046</a>)</li> <li>Preserve correlated generic-call inference (<a href="https://redirect.github.com/astral-sh/ruff/pull/28043">#28043</a>)</li> <li>Preserve invariant materialization constraints (<a href="https://redirect.github.com/astral-sh/ruff/pull/28047">#28047</a>)</li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/astral-sh/ty/commit/a553dcc1a4c464b22d6fff8c46d444e326e0ae2d"><code>a553dcc</code></a> Bump version to 0.0.75 (<a href="https://redirect.github.com/astral-sh/ty/issues/4396">#4396</a>)</li> <li><a href="https://github.com/astral-sh/ty/commit/26b1b50998e92a5f50c342dc4a1fed84db2f8ee4"><code>26b1b50</code></a> Update prek dependencies (<a href="https://redirect.github.com/astral-sh/ty/issues/4392">#4392</a>)</li> <li><a href="https://github.com/astral-sh/ty/commit/73d226ee4d5a9274bc559c50a244159b61dfa315"><code>73d226e</code></a> Fix documented Python package build command (<a href="https://redirect.github.com/astral-sh/ty/issues/4384">#4384</a>)</li> <li><a href="https://github.com/astral-sh/ty/commit/5e264dac66970648d461a3fc8f2ebebd30a44dc9"><code>5e264da</code></a> Link untyped-call tracking issue in migration guide (<a href="https://redirect.github.com/astral-sh/ty/issues/4382">#4382</a>)</li> <li><a href="https://github.com/astral-sh/ty/commit/00199f0aaa6a8cd264fea08eae4f3c3fe4451c17"><code>00199f0</code></a> Bump version to 0.0.74 (<a href="https://redirect.github.com/astral-sh/ty/issues/4351">#4351</a>)</li> <li><a href="https://github.com/astral-sh/ty/commit/5769062fa10db84f6c03145bdf42d0b8da9aeb63"><code>5769062</code></a> Update mypy/pyright comparison table (<a href="https://redirect.github.com/astral-sh/ty/issues/4165">#4165</a>)</li> <li><a href="https://github.com/astral-sh/ty/commit/4fcb06010f2219b80adf3e688964abd9b0d72866"><code>4fcb060</code></a> Remove Zulip from the PGO training (<a href="https://redirect.github.com/astral-sh/ty/issues/4336">#4336</a>)</li> <li><a href="https://github.com/astral-sh/ty/commit/a7a706442a710db4a93c1a79779dff5ec5824cf6"><code>a7a7064</code></a> Use the versions bot's ruleset bypass when merging (<a href="https://redirect.github.com/astral-sh/ty/issues/4333">#4333</a>)</li> <li><a href="https://github.com/astral-sh/ty/commit/01b0fab0093c64535a401bc2731f80d7f5b6a7a8"><code>01b0fab</code></a> Update prek dependencies (<a href="https://redirect.github.com/astral-sh/ty/issues/4325">#4325</a>)</li> <li><a href="https://github.com/astral-sh/ty/commit/deef42f517bfbacb2ffe673046bef1e206c6b8ab"><code>deef42f</code></a> Update dependency python to 3.14 (<a href="https://redirect.github.com/astral-sh/ty/issues/4326">#4326</a>)</li> <li>Additional commits viewable in <a href="https://github.com/astral-sh/ty/compare/0.0.66...0.0.75">compare view</a></li> </ul> </details> <br /> Updates `starlette` from 1.3.1 to 1.6.0 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/Kludex/starlette/releases">starlette's releases</a>.</em></p> <blockquote> <h2>Version 1.6.0</h2> <h2>What's Changed</h2> <ul> <li>Add <code>max_body_size</code> to <code>Starlette</code> and route classes by <a href="https://github.com/Kludex"><code>@Kludex</code></a> in <a href="https://redirect.github.com/Kludex/starlette/pull/3431">Kludex/starlette#3431</a></li> <li>Expose <code>http.response.debug</code> info via response extensions by <a href="https://github.com/y2kbugger"><code>@y2kbugger</code></a> in <a href="https://redirect.github.com/Kludex/starlette/pull/3130">Kludex/starlette#3130</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/y2kbugger"><code>@y2kbugger</code></a> made their first contribution in <a href="https://redirect.github.com/Kludex/starlette/pull/3130">Kludex/starlette#3130</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/Kludex/starlette/compare/1.5.1...1.6.0">https://github.com/Kludex/starlette/compare/1.5.1...1.6.0</a></p> <h2>Version 1.5.1</h2> <h2>What's Changed</h2> <ul> <li>Reject inverted single-byte Range like <code>bytes=5-4</code> by <a href="https://github.com/nikolauspschuetz"><code>@nikolauspschuetz</code></a> in <a href="https://redirect.github.com/encode/starlette/pull/3389">encode/starlette#3389</a></li> <li>Limit <code>FileResponse</code> to 100 ranges by <a href="https://github.com/Kludex"><code>@Kludex</code></a> in <a href="https://redirect.github.com/encode/starlette/pull/3430">encode/starlette#3430</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/encode/starlette/compare/1.5.0...1.5.1">https://github.com/encode/starlette/compare/1.5.0...1.5.1</a></p> <h2>Version 1.5.0</h2> <p>This release is all about giving <code>GZipMiddleware</code> some love. 🗜️</p> <h2>What's Changed</h2> <ul> <li>Add <code>exclude_content_types</code> parameter to <code>GZipMiddleware</code> by <a href="https://github.com/Kludex"><code>@Kludex</code></a> in <a href="https://redirect.github.com/encode/starlette/pull/3418">encode/starlette#3418</a></li> <li>Flush GZip output for each streamed chunk by <a href="https://github.com/Kludex"><code>@Kludex</code></a> in <a href="https://redirect.github.com/encode/starlette/pull/3419">encode/starlette#3419</a></li> <li>Skip compression of partial responses in <code>GZipMiddleware</code> by <a href="https://github.com/Kludex"><code>@Kludex</code></a> in <a href="https://redirect.github.com/encode/starlette/pull/3420">encode/starlette#3420</a></li> <li>Expand default excluded content types in <code>GZipMiddleware</code> by <a href="https://github.com/Kludex"><code>@Kludex</code></a> in <a href="https://redirect.github.com/encode/starlette/pull/3421">encode/starlette#3421</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/encode/starlette/compare/1.4.1...1.5.0">https://github.com/encode/starlette/compare/1.4.1...1.5.0</a></p> <h2>Version 1.4.1</h2> <h2>What's Changed</h2> <ul> <li>Default <code>thread_minimum_size</code> in <code>GZipResponder</code> by <a href="https://github.com/Kludex"><code>@Kludex</code></a> in <a href="https://redirect.github.com/Kludex/starlette/pull/3415">Kludex/starlette#3415</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/Kludex/starlette/compare/1.4.0...1.4.1">https://github.com/Kludex/starlette/compare/1.4.0...1.4.1</a></p> <h2>Version 1.4.0</h2> <h2>What's Changed</h2> <ul> <li>Lazily allocate GZipMiddleware compression resources by <a href="https://github.com/Kludex"><code>@Kludex</code></a> in <a href="https://redirect.github.com/Kludex/starlette/pull/3407">Kludex/starlette#3407</a></li> <li>Use <code>zlib.compressobj</code> instead of <code>GzipFile</code> in <code>GZipMiddleware</code> by <a href="https://github.com/Kludex"><code>@Kludex</code></a> in <a href="https://redirect.github.com/Kludex/starlette/pull/3411">Kludex/starlette#3411</a></li> <li>Offload large GZip compression by <a href="https://github.com/Kludex"><code>@Kludex</code></a> in <a href="https://redirect.github.com/Kludex/starlette/pull/3410">Kludex/starlette#3410</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/benberryallwood"><code>@benberryallwood</code></a> made their first contribution in <a href="https://redirect.github.com/Kludex/starlette/pull/3334">Kludex/starlette#3334</a></li> <li><a href="https://github.com/lkk7"><code>@lkk7</code></a> made their first contribution in <a href="https://redirect.github.com/Kludex/starlette/pull/3359">Kludex/starlette#3359</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/Kludex/starlette/compare/1.3.1...1.4.0">https://github.com/Kludex/starlette/compare/1.3.1...1.4.0</a></p> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/Kludex/starlette/blob/main/docs/release-notes.md">starlette's changelog</a>.</em></p> <blockquote> <h2>1.6.0 (August 8, 2026)</h2> <h4>Added</h4> <ul> <li>Add <code>max_body_size</code> to <code>Starlette</code> and route classes <a href="https://redirect.github.com/encode/starlette/pull/3431">#3431</a>.</li> <li>Expose <code>http.response.debug</code> information via response extensions <a href="https://redirect.github.com/encode/starlette/pull/3130">#3130</a>.</li> </ul> <h2>1.5.1 (August 8, 2026)</h2> <h4>Fixed</h4> <ul> <li>Reject inverted single-byte ranges in <code>FileResponse</code> <a href="https://redirect.github.com/encode/starlette/pull/3389">#3389</a>.</li> <li>Limit <code>FileResponse</code> to 100 ranges <a href="https://redirect.github.com/encode/starlette/pull/3430">#3430</a>.</li> </ul> <h2>1.5.0 (August 8, 2026)</h2> <h4>Added</h4> <ul> <li>Add <code>exclude_content_types</code> parameter to <code>GZipMiddleware</code> <a href="https://redirect.github.com/encode/starlette/pull/3418">#3418</a>.</li> </ul> <h4>Changed</h4> <ul> <li>Expand default excluded content types in <code>GZipMiddleware</code> <a href="https://redirect.github.com/encode/starlette/pull/3421">#3421</a>.</li> </ul> <h4>Fixed</h4> <ul> <li>Flush GZip output for each streamed chunk <a href="https://redirect.github.com/encode/starlette/pull/3419">#3419</a>.</li> <li>Skip compression of partial responses in <code>GZipMiddleware</code> <a href="https://redirect.github.com/encode/starlette/pull/3420">#3420</a>.</li> </ul> <h2>1.4.1 (August 5, 2026)</h2> <h4>Fixed</h4> <ul> <li>Default <code>thread_minimum_size</code> to 128 KiB in <code>GZipResponder</code>, keeping it usable without the new keyword argument <a href="https://redirect.github.com/encode/starlette/pull/3415">#3415</a>.</li> </ul> <h2>1.4.0 (August 5, 2026)</h2> <h4>Added</h4> <ul> <li>Offload large GZip compression to a worker thread, keeping the event loop responsive. <code>GZipMiddleware</code> accepts a new <code>thread_minimum_size</code> parameter (default 128 KiB) controlling the minimum body chunk size compressed in a thread <a href="https://redirect.github.com/encode/starlette/pull/3410">#3410</a>.</li> </ul> <h4>Changed</h4> <ul> <li>Use <code>zlib.compressobj</code> instead of <code>GzipFile</code> in <code>GZipMiddleware</code>, reducing memory usage during compression <a href="https://redirect.github.com/encode/starlette/pull/3411">#3411</a>.</li> <li>Lazily allocate <code>GZipMiddleware</code> compression resources, avoiding compressor allocation for responses that are never compressed <a href="https://redirect.github.com/encode/starlette/pull/3407">#3407</a>.</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/Kludex/starlette/commit/4f250d6b814587e20c5365f0a5f0c4d42bcb929f"><code>4f250d6</code></a> Version 1.6.0 (<a href="https://redirect.github.com/Kludex/starlette/issues/3434">#3434</a>)</li> <li><a href="https://github.com/Kludex/starlette/commit/9eea41ad3c26ad21b9bdfe4578c1cdad6c9b9ac2"><code>9eea41a</code></a> Expose <code>http.response.debug</code> info via response extensions (<a href="https://redirect.github.com/Kludex/starlette/issues/3130">#3130</a>)</li> <li><a href="https://github.com/Kludex/starlette/commit/38f8999a229610b36f39d11f67d80515a15c6330"><code>38f8999</code></a> Add <code>max_body_size</code> to <code>Starlette</code> and route classes (<a href="https://redirect.github.com/Kludex/starlette/issues/3431">#3431</a>)</li> <li><a href="https://github.com/Kludex/starlette/commit/c41236c03868fb3779a64101f4ea88cd47877e23"><code>c41236c</code></a> Version 1.5.1 (<a href="https://redirect.github.com/Kludex/starlette/issues/3432">#3432</a>)</li> <li><a href="https://github.com/Kludex/starlette/commit/9c500db197859dba4a8db13fa8e7d2c55de8152c"><code>9c500db</code></a> Limit <code>FileResponse</code> to 100 ranges (<a href="https://redirect.github.com/Kludex/starlette/issues/3430">#3430</a>)</li> <li><a href="https://github.com/Kludex/starlette/commit/78ae82cad482fe66a73fb1217f0a6609b3f998f6"><code>78ae82c</code></a> Reject inverted single-byte Range like bytes=5-4 (<a href="https://redirect.github.com/Kludex/starlette/issues/3389">#3389</a>)</li> <li><a href="https://github.com/Kludex/starlette/commit/c1d6edaf43920104d4389e983c3e0314f6af14cf"><code>c1d6eda</code></a> chore(deps): bump pymdown-extensions from 11.0 to 11.0.1 (<a href="https://redirect.github.com/Kludex/starlette/issues/3429">#3429</a>)</li> <li><a href="https://github.com/Kludex/starlette/commit/ee66ca48418780d6415d231851b31464febc32de"><code>ee66ca4</code></a> chore(deps): bump the python-packages group across 1 directory with 8 updates...</li> <li><a href="https://github.com/Kludex/starlette/commit/00d10167523f819d39d5ca36732348d58645a447"><code>00d1016</code></a> fix(tests): skip test_staticfiles_filename_too_long on Windows where os.pathc...</li> <li><a href="https://github.com/Kludex/starlette/commit/d96887ea7b49db3d1d15994be6438c7fe99936f4"><code>d96887e</code></a> Add Pydantic Logfire banner to the docs (<a href="https://redirect.github.com/Kludex/starlette/issues/3428">#3428</a>)</li> <li>Additional commits viewable in <a href="https://github.com/Kludex/starlette/compare/1.3.1...1.6.0">compare view</a></li> </ul> </details> <br /> Updates `pydantic` from 2.13.4 to 2.13.5 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/pydantic/pydantic/releases">pydantic's releases</a>.</em></p> <blockquote> <h2>v2.13.5 (2026-08-28)</h2> <h3>What's Changed</h3> <h4>Fixes</h4> <ul> <li>Allow reuse of validators when plugins are set by <a href="https://github.com/Viicos"><code>@Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13535">#13535</a></li> <li>Fix missing GC traversal on some <code>pydantic-core</code> struct fields by <a href="https://github.com/Viicos"><code>@Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13624">#13624</a></li> <li>Fix missing GC traversal in <code>pydantic-core</code> for <code>GeneralFieldsSerializer</code> by <a href="https://github.com/Viicos"><code>@Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13629">#13629</a></li> <li>Count validated model fields once in smart unions by <a href="https://github.com/tamird"><code>@tamird</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13731">#13731</a></li> </ul> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/pydantic/pydantic/blob/v2.13.5/HISTORY.md">pydantic's changelog</a>.</em></p> <blockquote> <h2>v2.13.5 (2026-08-28)</h2> <p><a href="https://github.com/pydantic/pydantic/releases/tag/v2.13.5">GitHub release</a></p> <h3>What's Changed</h3> <h4>Fixes</h4> <ul> <li>Allow reuse of validators when plugins are set by <a href="https://github.com/Viicos"><code>@Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13535">#13535</a></li> <li>Fix missing GC traversal on some <code>pydantic-core</code> struct fields by <a href="https://github.com/Viicos"><code>@Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13624">#13624</a></li> <li>Fix missing GC traversal in <code>pydantic-core</code> for <code>GeneralFieldsSerializer</code> by <a href="https://github.com/Viicos"><code>@Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13629">#13629</a></li> <li>Count validated model fields once in smart unions by <a href="https://github.com/tamird"><code>@tamird</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13731">#13731</a></li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/pydantic/pydantic/commit/001dea020e0809844e5b17666432c9135a976f46"><code>001dea0</code></a> Bump <code>pypa/gh-action-pypi-publish</code> action to v1.14.2</li> <li><a href="https://github.com/pydantic/pydantic/commit/558379fee957fad25858e33596146c72d5674843"><code>558379f</code></a> Bump twine to v7.0.0</li> <li><a href="https://github.com/pydantic/pydantic/commit/2cfd5d3d2ae721ea882c045f83b7f7a4003aab01"><code>2cfd5d3</code></a> Do not check for docs build</li> <li><a href="https://github.com/pydantic/pydantic/commit/a735bee5c64e1fe50785c376adaf1275e752b8ac"><code>a735bee</code></a> Fix more Clippy lints</li> <li><a href="https://github.com/pydantic/pydantic/commit/7eed4a16b87afaced4d0eafae230453de3410d64"><code>7eed4a1</code></a> Fix Clippy 0.1.95 warnings</li> <li><a href="https://github.com/pydantic/pydantic/commit/b353bbb20a4989b4c033e2be2b4a40d36178b16a"><code>b353bbb</code></a> Prepare release v2.13.5</li> <li><a href="https://github.com/pydantic/pydantic/commit/63d2cccd6e760da6bbc7fe81a53cb2bde3768f5e"><code>63d2ccc</code></a> Count validated model fields once in smart unions</li> <li><a href="https://github.com/pydantic/pydantic/commit/a53ec2ed342fda4e6aa3684399d04ea57be9a6e4"><code>a53ec2e</code></a> Speed up PyPy CI tests</li> <li><a href="https://github.com/pydantic/pydantic/commit/d65e0f96e890ca82f4b66a15d5b3bd65af2c661c"><code>d65e0f9</code></a> Workaround circular import error in Mypy</li> <li><a href="https://github.com/pydantic/pydantic/commit/47a6dbf8ad6216211bd0df6ec5c2c6c6ed905b72"><code>47a6dbf</code></a> Fix missing GC traversal in <code>pydantic-core</code> for <code>GeneralFieldsSerializer</code></li> <li>Additional commits viewable in <a href="https://github.com/pydantic/pydantic/compare/v2.13.4...v2.13.5">compare view</a></li> </ul> </details> <br /> Made by [Open SWE](https://openswe.vercel.app/agents/0758e158-6746-5095-b8a0-7f69acf36b73) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
853 lines
33 KiB
Python
853 lines
33 KiB
Python
"""Tests for SyncThreadStream — Tasks 9.1 through 9.6."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import queue
|
|
import re
|
|
import threading
|
|
import time
|
|
import uuid
|
|
from collections.abc import Iterator
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from typing import Any
|
|
|
|
import httpx
|
|
import orjson
|
|
import pytest
|
|
|
|
import langgraph_sdk.stream.sync_controller as _ctrl_mod
|
|
from langgraph_sdk._sync.http import SyncHttpClient
|
|
from langgraph_sdk._sync.threads import SyncThreadsClient
|
|
from langgraph_sdk.stream.sync_controller import SyncStreamController
|
|
from langgraph_sdk.stream.transport.sync_http import (
|
|
SyncEventStreamHandle,
|
|
SyncProtocolSseTransport,
|
|
)
|
|
from streaming._events import (
|
|
checkpoints_event,
|
|
custom_event,
|
|
lifecycle_completed_event,
|
|
lifecycle_event,
|
|
lifecycle_started_event,
|
|
message_finish_event,
|
|
message_start_event,
|
|
message_text_delta_event,
|
|
message_text_finish_event,
|
|
tasks_start_event,
|
|
tool_finished_event,
|
|
tool_started_event,
|
|
updates_event,
|
|
values_event,
|
|
)
|
|
from streaming._sync_fake_server import SyncFakeServer, SyncStreamScript
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Task 9.1 — run_start_gate
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_sync_subscribe_before_run_start_waits_on_gate():
|
|
"""A subscribe issued before run.start completes must block until the
|
|
gate is set, mirroring async behavior."""
|
|
fake = SyncFakeServer()
|
|
# Lifecycle + fanout streams: empty so threads terminate cleanly.
|
|
fake.script_sequence(
|
|
[
|
|
SyncStreamScript(events=[]), # lifecycle watcher
|
|
SyncStreamScript(events=[]), # first subscribe
|
|
]
|
|
)
|
|
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
controller = thread._controller
|
|
assert controller is not None
|
|
|
|
started = threading.Event()
|
|
|
|
def slow_subscriber() -> None:
|
|
started.set()
|
|
list(thread.subscribe(["values"]))
|
|
|
|
t = threading.Thread(target=slow_subscriber)
|
|
t.start()
|
|
started.wait(timeout=0.5)
|
|
|
|
# Set the gate manually (simulating run.start completing)
|
|
time.sleep(0.05)
|
|
assert controller._run_start_gate is not None
|
|
controller._run_start_gate.set()
|
|
|
|
t.join(timeout=2.0)
|
|
|
|
# The subscriber should have unblocked and terminated cleanly.
|
|
assert not t.is_alive(), "subscriber thread should have terminated"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Task 9.2 — reconnect backoff + ready check
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_sync_reconnect_uses_backoff_between_attempts(monkeypatch):
|
|
"""_reconnect_shared_stream sleeps between retry attempts with exp+jitter
|
|
backoff, mirroring the async reconnect behavior."""
|
|
|
|
sleeps: list[float] = []
|
|
monkeypatch.setattr(_ctrl_mod.time, "sleep", lambda d: sleeps.append(d))
|
|
|
|
class _FailingTransport(SyncProtocolSseTransport):
|
|
"""Transport that always raises on open_event_stream."""
|
|
|
|
def open_event_stream(self, params: dict) -> SyncEventStreamHandle: # noqa: ARG002
|
|
raise RuntimeError("scripted transport failure")
|
|
|
|
with httpx.Client(base_url="http://test") as raw:
|
|
transport = _FailingTransport(client=raw, thread_id="t-1")
|
|
controller = SyncStreamController(transport, max_reconnect_attempts=5)
|
|
controller._shared_stream_filter = {"channels": ["values"]}
|
|
result = controller._reconnect_shared_stream()
|
|
|
|
assert result is False, "all attempts should have failed"
|
|
# Attempts 0..4 → sleeps before attempts 1..4 → 4 sleeps
|
|
assert len(sleeps) == 4, f"Expected 4 sleeps, got {sleeps}"
|
|
# Backoff should grow (each delay is larger than previous, ignoring jitter)
|
|
delays_without_jitter = [0.1 * (2**i) for i in range(4)]
|
|
for i, (sleep, expected_base) in enumerate(
|
|
zip(sleeps, delays_without_jitter, strict=False)
|
|
):
|
|
assert sleep >= expected_base, (
|
|
f"sleep[{i}]={sleep} < expected base {expected_base}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Task 9.3 — rotation drains buffered events from old stream
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_sync_rotation_does_not_lose_buffered_events():
|
|
"""When the shared stream rotates, old-stream events already in the queue
|
|
are not dropped. _drain_and_close dispatches remaining events from the
|
|
old handle to subscribers before closing it."""
|
|
|
|
event_a = values_event(seq=1, counter=1)
|
|
|
|
class _ScriptedTransport(SyncProtocolSseTransport):
|
|
"""First call produces event_a; second call produces an empty stream."""
|
|
|
|
def open_event_stream(self, params: dict) -> SyncEventStreamHandle: # noqa: ARG002
|
|
def _gen_a() -> Iterator[Any]:
|
|
yield event_a
|
|
|
|
def _gen_empty() -> Iterator[Any]:
|
|
return
|
|
yield # pragma: no cover
|
|
|
|
# Alternate: first call → a, second → empty.
|
|
if not hasattr(self, "_call_count"):
|
|
self._call_count = 0
|
|
self._call_count += 1
|
|
events_gen: Iterator[Any] = (
|
|
_gen_a() if self._call_count == 1 else _gen_empty()
|
|
)
|
|
return SyncEventStreamHandle(
|
|
events=events_gen,
|
|
error=lambda: None,
|
|
close=lambda: None,
|
|
)
|
|
|
|
with httpx.Client(base_url="http://test") as raw:
|
|
transport = _ScriptedTransport(client=raw, thread_id="t-1")
|
|
controller = SyncStreamController(transport)
|
|
sub = controller.register_subscription({"channels": ["values"]})
|
|
|
|
# First reconcile — opens old stream (event_a available immediately).
|
|
controller.reconcile_stream({"channels": ["values"]})
|
|
# Do NOT start fanout; let reconcile_stream cause a rotation directly.
|
|
|
|
# Second reconcile: rotates to empty stream; drain thread handles old.
|
|
controller.reconcile_stream({"channels": ["values", "updates"]})
|
|
|
|
# Start fanout AFTER rotation (picks up the new empty stream).
|
|
controller.ensure_fanout_running()
|
|
|
|
# Allow drain thread to finish before collecting results.
|
|
controller.close()
|
|
|
|
received = []
|
|
while True:
|
|
try:
|
|
item = sub.queue.get_nowait()
|
|
if item is None:
|
|
continue
|
|
received.append(item)
|
|
except queue.Empty:
|
|
break
|
|
|
|
seqs = [e.get("seq") for e in received]
|
|
assert 1 in seqs, f"event_a (seq=1) not received via drain; got seqs={seqs}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Task 9.4 — _next_command_id lock
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_sync_concurrent_commands_do_not_share_command_id():
|
|
"""50 concurrent threads calling _send_command must each get a unique id."""
|
|
|
|
captured_ids: list[int] = []
|
|
ids_lock = threading.Lock()
|
|
|
|
class _CapturingTransport(SyncProtocolSseTransport):
|
|
"""Captures command ids; always returns success."""
|
|
|
|
def send_command(self, command: dict) -> dict:
|
|
with ids_lock:
|
|
captured_ids.append(command["id"])
|
|
return {"type": "success", "id": command["id"], "result": {}}
|
|
|
|
def open_event_stream(self, params: dict) -> SyncEventStreamHandle: # noqa: ARG002
|
|
def _gen() -> Iterator[Any]:
|
|
return
|
|
yield
|
|
|
|
return SyncEventStreamHandle(
|
|
events=_gen(), error=lambda: None, close=lambda: None
|
|
)
|
|
|
|
fake = SyncFakeServer()
|
|
fake.script_sequence([SyncStreamScript(events=[])])
|
|
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads_client = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads_client.stream(thread_id="t-cmd", assistant_id="agent") as stream:
|
|
# Pre-set gate so _send_command doesn't wait.
|
|
if stream._controller and stream._controller._run_start_gate:
|
|
stream._controller._run_start_gate.set()
|
|
# Replace transport with capturing transport.
|
|
capture_transport = _CapturingTransport(client=raw, thread_id="t-cmd")
|
|
stream._transport = capture_transport
|
|
|
|
with ThreadPoolExecutor(max_workers=50) as ex:
|
|
futures = [
|
|
ex.submit(stream._send_command, "noop", {}) for _ in range(50)
|
|
]
|
|
for f in futures:
|
|
f.result()
|
|
|
|
assert len(set(captured_ids)) == 50, (
|
|
f"Expected 50 unique command ids, got {len(set(captured_ids))} unique "
|
|
f"out of {len(captured_ids)} total: {sorted(captured_ids)}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Task 9.5 — sync events returns fresh iterator per access
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_sync_events_returns_fresh_iterator_each_access():
|
|
"""Two accesses of `thread.events` yield independent subscriptions,
|
|
mirroring the async semantics where each access opens a new subscriber."""
|
|
fake = SyncFakeServer()
|
|
|
|
event_1 = values_event(seq=1, counter=1)
|
|
fake.script_sequence(
|
|
[
|
|
SyncStreamScript(events=[]), # lifecycle watcher
|
|
SyncStreamScript(events=[event_1]), # first events access
|
|
SyncStreamScript(events=[event_1]), # second events access
|
|
]
|
|
)
|
|
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads_client = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads_client.stream(thread_id="t-5", assistant_id="agent") as thread:
|
|
# Pre-set gate.
|
|
if thread._controller and thread._controller._run_start_gate:
|
|
thread._controller._run_start_gate.set()
|
|
|
|
iter1 = thread.events
|
|
iter2 = thread.events
|
|
|
|
# They must be independent objects (different subscription iterators).
|
|
assert iter1 is not iter2
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Task 9.6 — close ordering: fail active streams before controller close
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_close_unblocks_active_subscription_before_lifecycle_join():
|
|
"""close() must send None to active subscriptions BEFORE joining the
|
|
lifecycle watcher thread, so callers wake quickly even if the watcher
|
|
thread blocks for up to 1s."""
|
|
|
|
# Gate that keeps the lifecycle watcher thread alive for 0.4s.
|
|
lifecycle_block = threading.Event()
|
|
unblock_times: list[float] = []
|
|
close_times: list[float] = []
|
|
|
|
class _BlockingFakeServer(SyncFakeServer):
|
|
"""Lifecycle stream blocks until gate set; subscribe stream is empty."""
|
|
|
|
def _handle(self, request: httpx.Request) -> httpx.Response:
|
|
path = request.url.path
|
|
if path.endswith("/stream/events"):
|
|
body = orjson.loads(request.content)
|
|
channels = body.get("channels", [])
|
|
if "lifecycle" in channels:
|
|
# Block lifecycle watcher for 0.4s.
|
|
lifecycle_block.wait(timeout=0.4)
|
|
return super()._handle(request)
|
|
|
|
fake = _BlockingFakeServer()
|
|
fake.script_sequence(
|
|
[
|
|
SyncStreamScript(events=[]), # lifecycle watcher
|
|
SyncStreamScript(events=[]), # subscribe fanout stream
|
|
]
|
|
)
|
|
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads_client = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads_client.stream(thread_id="t-6", assistant_id="agent") as thread:
|
|
if thread._controller and thread._controller._run_start_gate:
|
|
thread._controller._run_start_gate.set()
|
|
|
|
assert thread._controller is not None
|
|
sub = thread._controller.register_subscription({"channels": ["values"]})
|
|
thread._controller.reconcile_stream({"channels": ["values"]})
|
|
thread._controller.ensure_fanout_running()
|
|
|
|
consumer_ready = threading.Event()
|
|
|
|
def _consume() -> None:
|
|
consumer_ready.set()
|
|
while True:
|
|
try:
|
|
item = sub.queue.get(timeout=2.0)
|
|
if item is None:
|
|
unblock_times.append(time.monotonic())
|
|
return
|
|
except queue.Empty:
|
|
return
|
|
|
|
t = threading.Thread(target=_consume)
|
|
t.start()
|
|
consumer_ready.wait(timeout=1.0)
|
|
time.sleep(0.02)
|
|
|
|
close_times.append(time.monotonic())
|
|
# __exit__ calls close() here.
|
|
|
|
lifecycle_block.set() # Unblock watcher so test can finish.
|
|
t.join(timeout=2.0)
|
|
assert not t.is_alive(), "consumer thread should have unblocked"
|
|
assert unblock_times, "consumer never received sentinel"
|
|
elapsed = unblock_times[0] - close_times[0]
|
|
# With controller closed BEFORE lifecycle join, sentinel arrives fast.
|
|
# Lifecycle watcher blocks for 0.4s but that should not delay the sentinel.
|
|
assert elapsed < 0.3, (
|
|
f"consumer woke {elapsed:.3f}s after close() — "
|
|
"controller.close() should precede the lifecycle thread join"
|
|
)
|
|
|
|
|
|
def test_sync_thread_agent_get_tree_fetches_assistant_graph():
|
|
fake = SyncFakeServer()
|
|
fake.set_graph(
|
|
{
|
|
"nodes": [{"id": "agent", "type": "runnable", "data": {"name": "agent"}}],
|
|
"edges": [{"source": "agent", "target": "__end__"}],
|
|
}
|
|
)
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(
|
|
thread_id="t-1",
|
|
assistant_id="agent",
|
|
headers={"X-Custom-Header": "my-value"},
|
|
) as thread:
|
|
graph = thread.agent.get_tree(xray=True)
|
|
|
|
assert graph["nodes"][0]["id"] == "agent"
|
|
assert graph["edges"] == [{"source": "agent", "target": "__end__"}]
|
|
assert fake.graph_request_params == [{"xray": "true"}]
|
|
assert fake.graph_request_headers[0].get("x-custom-header") == "my-value"
|
|
|
|
|
|
def test_sync_thread_agent_get_tree_raises_after_close():
|
|
with httpx.Client(base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
stream = threads.stream(thread_id="t-1", assistant_id="agent")
|
|
stream.close()
|
|
with pytest.raises(RuntimeError, match="closed"):
|
|
stream.agent.get_tree()
|
|
|
|
|
|
def test_sync_extensions_projection_empty_name_raises():
|
|
with httpx.Client(base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
stream = threads.stream(thread_id="t-1", assistant_id="agent")
|
|
with pytest.raises(ValueError, match="non-empty"):
|
|
stream.extensions[""]
|
|
|
|
|
|
def test_sync_extensions_projection_closed_stream_yields_nothing():
|
|
with httpx.Client(base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
# Enter and immediately exit so _controller is set but _closed is True.
|
|
with threads.stream(thread_id="t-1", assistant_id="agent") as stream:
|
|
pass
|
|
payloads = list(stream.extensions["progress"])
|
|
assert payloads == []
|
|
|
|
|
|
def test_sync_threads_stream_mints_uuid4_when_thread_id_none():
|
|
with httpx.Client(base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
stream = threads.stream(assistant_id="agent")
|
|
assert re.fullmatch(
|
|
r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}",
|
|
stream.thread_id,
|
|
)
|
|
assert uuid.UUID(stream.thread_id).version == 4
|
|
|
|
|
|
def test_sync_run_start_sends_command():
|
|
|
|
fake = SyncFakeServer()
|
|
fake.script([lifecycle_completed_event(seq=1)])
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
result = thread.run.start(
|
|
input={"x": 1},
|
|
langsmith_tracing={"project_name": "replica-project"},
|
|
)
|
|
|
|
assert result == {"run_id": "run-1"}
|
|
assert fake.received_commands[0]["method"] == "run.start"
|
|
assert fake.received_commands[0]["params"]["assistant_id"] == "agent"
|
|
assert fake.received_commands[0]["params"]["langsmith_tracer"] == {
|
|
"project_name": "replica-project"
|
|
}
|
|
|
|
|
|
def test_sync_events_iterates_raw_events():
|
|
|
|
fake = SyncFakeServer()
|
|
fake.script([values_event(seq=1, counter=1)])
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
thread.run.start(input={})
|
|
events = list(thread.subscribe(["values"]))
|
|
|
|
assert events == [values_event(seq=1, counter=1)]
|
|
|
|
|
|
def test_sync_lifecycle_watcher_reconnects_with_since_after_transport_drop():
|
|
|
|
fake = SyncFakeServer()
|
|
fake.set_state({"ok": True})
|
|
fake.script_sequence(
|
|
[
|
|
SyncStreamScript(
|
|
events=[lifecycle_event(seq=1, phase="running")],
|
|
fail_after=1,
|
|
),
|
|
SyncStreamScript(events=[lifecycle_completed_event(seq=2)]),
|
|
]
|
|
)
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(thread_id="existing", assistant_id="agent") as thread:
|
|
terminal = thread._wait_for_run_done()
|
|
|
|
assert terminal.status == "completed"
|
|
assert terminal.error is None
|
|
assert fake.stream_request_bodies[1]["since"] == 1
|
|
|
|
|
|
def test_sync_threads_stream_accepts_websocket_transport_option():
|
|
with httpx.Client(base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
stream = threads.stream(
|
|
thread_id="t-1",
|
|
assistant_id="agent",
|
|
transport="websocket",
|
|
)
|
|
assert stream._transport_kind == "websocket"
|
|
|
|
|
|
def test_sync_threads_stream_rejects_unknown_transport_option():
|
|
|
|
with httpx.Client(base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with pytest.raises(ValueError, match="transport"):
|
|
threads.stream(
|
|
thread_id="t-1",
|
|
assistant_id="agent",
|
|
transport="bogus", # ty: ignore[invalid-argument-type]
|
|
)
|
|
|
|
|
|
def test_v3_streaming_sync_surface_smoke():
|
|
|
|
fake = SyncFakeServer()
|
|
fake.set_state({"final": True})
|
|
# Single script — projections consume events in parallel threads so all
|
|
# subscriptions are registered before SSE rotation could drop events.
|
|
# Mirrors the async smoke test's `asyncio.gather` pattern.
|
|
fake.script(
|
|
[
|
|
values_event(seq=1, values={"step": 1}),
|
|
message_start_event(seq=2, message_id="msg-1"),
|
|
message_text_delta_event(seq=3, text="hi", message_id="msg-1"),
|
|
message_text_finish_event(seq=4, text="hi", message_id="msg-1"),
|
|
message_finish_event(seq=5, message_id="msg-1"),
|
|
tool_started_event(seq=6, tool_call_id="call-1", tool_name="search"),
|
|
tool_finished_event(seq=7, tool_call_id="call-1", output={"ok": True}),
|
|
custom_event(seq=8, name="progress", step=1),
|
|
lifecycle_completed_event(seq=9),
|
|
]
|
|
)
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
start = thread.run.start(
|
|
input={"messages": [{"role": "user", "content": "hi"}]}
|
|
)
|
|
|
|
# Gate every reconcile_stream call on a barrier so that all four
|
|
# projection threads register their subscriptions before any
|
|
# reconcile widens (or rotates) the shared SSE. This mirrors the
|
|
# async smoke test's `asyncio.gather` pattern: every subscription
|
|
# is registered before the first SSE opens; one SSE covers all
|
|
# consumers and `_seen_event_ids` covers any subsequent reconnect.
|
|
controller = thread._controller
|
|
assert controller is not None
|
|
barrier = threading.Barrier(4)
|
|
real_reconcile = controller.reconcile_stream
|
|
|
|
def _gated_reconcile(candidate_filter):
|
|
barrier.wait(timeout=10)
|
|
return real_reconcile(candidate_filter)
|
|
|
|
controller.reconcile_stream = _gated_reconcile # ty: ignore[invalid-assignment]
|
|
|
|
results: dict[str, object] = {}
|
|
errors: list[BaseException] = []
|
|
|
|
def _run_values() -> None:
|
|
try:
|
|
for v in thread.values:
|
|
results["values"] = v
|
|
return
|
|
except BaseException as err: # pragma: no cover - propagated
|
|
errors.append(err)
|
|
|
|
def _run_messages() -> None:
|
|
try:
|
|
results["messages"] = list(thread.messages)
|
|
except BaseException as err: # pragma: no cover - propagated
|
|
errors.append(err)
|
|
|
|
def _run_tools() -> None:
|
|
try:
|
|
results["tools"] = list(thread.tool_calls)
|
|
except BaseException as err: # pragma: no cover - propagated
|
|
errors.append(err)
|
|
|
|
def _run_progress() -> None:
|
|
try:
|
|
results["progress"] = list(thread.extensions["progress"])
|
|
except BaseException as err: # pragma: no cover - propagated
|
|
errors.append(err)
|
|
|
|
workers = [
|
|
threading.Thread(target=_run_values),
|
|
threading.Thread(target=_run_messages),
|
|
threading.Thread(target=_run_tools),
|
|
threading.Thread(target=_run_progress),
|
|
]
|
|
for w in workers:
|
|
w.start()
|
|
for w in workers:
|
|
w.join(timeout=10)
|
|
assert not w.is_alive(), "smoke worker thread hung"
|
|
controller.reconcile_stream = real_reconcile # ty: ignore[invalid-assignment]
|
|
assert not errors, errors
|
|
final = thread.output
|
|
|
|
assert start == {"run_id": "run-1"}
|
|
assert results["values"] == fake.state["values"]
|
|
messages_result = results["messages"]
|
|
assert isinstance(messages_result, list)
|
|
assert [str(m.text) for m in messages_result] == ["hi"]
|
|
tools_result = results["tools"]
|
|
assert isinstance(tools_result, list)
|
|
assert tools_result[0].name == "search"
|
|
assert results["progress"] == [{"name": "progress", "step": 1}]
|
|
assert final == {"final": True}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# interleave_projections tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_interleave_projections_single_channel_values():
|
|
|
|
fake = SyncFakeServer()
|
|
fake.set_state({"counter": 0})
|
|
fake.script(
|
|
[
|
|
lifecycle_started_event(seq=0),
|
|
values_event(seq=1, counter=1),
|
|
values_event(seq=2, counter=2),
|
|
lifecycle_completed_event(seq=3),
|
|
]
|
|
)
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
thread.run.start(input={})
|
|
items = []
|
|
for ch, item in thread.interleave_projections(["values"]):
|
|
items.append((ch, item))
|
|
assert ("values", {"counter": 1}) in items
|
|
assert ("values", {"counter": 2}) in items
|
|
assert all(ch == "values" for ch, _ in items)
|
|
|
|
|
|
def test_interleave_projections_values_and_messages_arrival_order():
|
|
|
|
fake = SyncFakeServer()
|
|
fake.set_state({"counter": 0})
|
|
fake.script(
|
|
[
|
|
lifecycle_started_event(seq=0),
|
|
values_event(seq=1, counter=1),
|
|
message_start_event(seq=2, message_id="m-1"),
|
|
values_event(seq=3, counter=2),
|
|
message_finish_event(seq=4, message_id="m-1"),
|
|
lifecycle_completed_event(seq=5),
|
|
]
|
|
)
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
thread.run.start(input={})
|
|
order = []
|
|
for ch, _ in thread.interleave_projections(["values", "messages"]):
|
|
order.append(ch)
|
|
if len(order) >= 3:
|
|
break
|
|
assert order[:3] == ["values", "messages", "values"]
|
|
|
|
|
|
def test_interleave_projections_mixes_builtin_and_extension():
|
|
|
|
fake = SyncFakeServer()
|
|
fake.set_state({"counter": 0})
|
|
fake.script(
|
|
[
|
|
lifecycle_started_event(seq=0),
|
|
values_event(seq=1, counter=1),
|
|
custom_event(seq=2, name="foo", hello="world"),
|
|
lifecycle_completed_event(seq=3),
|
|
]
|
|
)
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
thread.run.start(input={})
|
|
items = []
|
|
for ch, item in thread.interleave_projections(["values", "foo"]):
|
|
items.append((ch, item))
|
|
assert ("values", {"counter": 1}) in items
|
|
assert ("foo", {"name": "foo", "hello": "world"}) in items
|
|
|
|
|
|
def test_interleave_projections_tool_calls_uses_public_name():
|
|
|
|
fake = SyncFakeServer()
|
|
fake.set_state({})
|
|
fake.script(
|
|
[
|
|
lifecycle_started_event(seq=0),
|
|
tool_started_event(seq=1, tool_call_id="call-1", tool_name="search"),
|
|
tool_finished_event(seq=2, tool_call_id="call-1", output={"ok": True}),
|
|
lifecycle_completed_event(seq=3),
|
|
]
|
|
)
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
thread.run.start(input={})
|
|
names = []
|
|
handle = None
|
|
for ch, item in thread.interleave_projections(["tool_calls"]):
|
|
names.append(ch)
|
|
if handle is None:
|
|
handle = item
|
|
break
|
|
assert names == ["tool_calls"]
|
|
assert handle is not None
|
|
assert handle.tool_call_id == "call-1"
|
|
|
|
|
|
def test_interleave_projections_subgraphs_discovers_child():
|
|
|
|
fake = SyncFakeServer()
|
|
fake.set_state({})
|
|
fake.script(
|
|
[
|
|
lifecycle_started_event(seq=0),
|
|
lifecycle_started_event(seq=1, namespace=["child"]),
|
|
lifecycle_completed_event(seq=2),
|
|
]
|
|
)
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
thread.run.start(input={})
|
|
discovered = []
|
|
for ch, handle in thread.interleave_projections(["subgraphs"]):
|
|
discovered.append((ch, handle.path))
|
|
assert ("subgraphs", ("child",)) in discovered
|
|
|
|
|
|
def test_interleave_projections_inflight_tool_call_failed_on_break():
|
|
"""A tool handle held past an early break is failed in teardown, never left hanging."""
|
|
|
|
fake = SyncFakeServer()
|
|
fake.set_state({})
|
|
fake.script(
|
|
[
|
|
lifecycle_started_event(seq=0),
|
|
tool_started_event(seq=1, tool_call_id="call-1", tool_name="search"),
|
|
# no tool-finished: the call is still in flight when the consumer breaks
|
|
lifecycle_completed_event(seq=2),
|
|
]
|
|
)
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
thread.run.start(input={})
|
|
handle = None
|
|
for _, item in thread.interleave_projections(["tool_calls"]):
|
|
handle = item
|
|
break
|
|
assert handle is not None
|
|
# Without teardown finalization this blocks forever; the bounded
|
|
# timeout turns a regression into a TimeoutError, not a RuntimeError.
|
|
with pytest.raises(RuntimeError):
|
|
handle._result.result(timeout=2)
|
|
|
|
|
|
def test_interleave_projections_inflight_subgraph_finished_on_terminal():
|
|
"""A discovered subgraph child with no terminal tasks-result is force-completed."""
|
|
|
|
fake = SyncFakeServer()
|
|
fake.set_state({})
|
|
fake.script(
|
|
[
|
|
lifecycle_started_event(seq=0),
|
|
lifecycle_started_event(seq=1, namespace=["child"]),
|
|
# no tasks-result for the child: it is still "started" at run end
|
|
lifecycle_completed_event(seq=2),
|
|
]
|
|
)
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
thread.run.start(input={})
|
|
child = None
|
|
for _, handle in thread.interleave_projections(["subgraphs"]):
|
|
child = handle
|
|
assert child is not None
|
|
assert child.status == "completed"
|
|
|
|
|
|
@pytest.mark.parametrize("channel", ["lifecycle", "tools", "input"])
|
|
def test_interleave_projections_rejects_reserved_channel(channel):
|
|
"""Reserved protocol channel names raise instead of silently no-op'ing.
|
|
|
|
`infer_channel` treats these as first-class methods, but they have no
|
|
interleave decoder, so routing them to the extension/`custom:` fallback
|
|
would subscribe to a channel that never matches and yield nothing. Fail
|
|
closed. (`updates`/`checkpoints`/`tasks` are supported and tested below.)
|
|
"""
|
|
|
|
fake = SyncFakeServer()
|
|
fake.set_state({})
|
|
fake.script([lifecycle_started_event(seq=0), lifecycle_completed_event(seq=1)])
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with (
|
|
threads.stream(thread_id="t-1", assistant_id="agent") as thread,
|
|
pytest.raises(ValueError, match=channel),
|
|
):
|
|
for _ in thread.interleave_projections([channel]):
|
|
pass
|
|
|
|
|
|
def test_interleave_projections_data_channels_yield_payloads():
|
|
"""`updates`/`checkpoints`/`tasks` yield their raw `params.data` payloads."""
|
|
|
|
fake = SyncFakeServer()
|
|
fake.set_state({})
|
|
fake.script(
|
|
[
|
|
lifecycle_started_event(seq=0),
|
|
updates_event(seq=1, node={"v": 1}),
|
|
checkpoints_event(seq=2, ts="t-0", v=4),
|
|
tasks_start_event(seq=3, task_id="task-9"),
|
|
lifecycle_completed_event(seq=4),
|
|
]
|
|
)
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
thread.run.start(input={})
|
|
items = list(
|
|
thread.interleave_projections(["updates", "checkpoints", "tasks"])
|
|
)
|
|
assert ("updates", {"node": {"v": 1}}) in items
|
|
assert ("checkpoints", {"ts": "t-0", "v": 4}) in items
|
|
assert any(ch == "tasks" and item.get("id") == "task-9" for ch, item in items)
|
|
|
|
|
|
def test_interleave_projections_data_channel_scoped_to_root_namespace():
|
|
"""A child-namespace checkpoint must not leak into a root interleave."""
|
|
|
|
fake = SyncFakeServer()
|
|
fake.set_state({"counter": 0})
|
|
fake.script(
|
|
[
|
|
lifecycle_started_event(seq=0),
|
|
checkpoints_event(seq=1, namespace=["child"], scope="child"),
|
|
checkpoints_event(seq=2, scope="root"),
|
|
lifecycle_completed_event(seq=3),
|
|
]
|
|
)
|
|
with httpx.Client(transport=fake.transport, base_url="http://test") as raw:
|
|
threads = SyncThreadsClient(SyncHttpClient(raw))
|
|
with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
thread.run.start(input={})
|
|
checkpoints = [
|
|
item
|
|
for ch, item in thread.interleave_projections(["values", "checkpoints"])
|
|
if ch == "checkpoints"
|
|
]
|
|
assert {"scope": "root"} in checkpoints
|
|
assert {"scope": "child"} not in checkpoints
|