fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone()
add_langgraph_fastapi_endpoint clones the agent on every request, and
LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base
class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only
signature, so ag-ui-langgraph 0.0.43 - which began forwarding
enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events -
made every request 500 on the documented LangGraph + FastAPI quickstart:
TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__
accepts (name, graph, description, config) as keyword arguments:
LangGraphAGUIAgent.__init__() got an unexpected keyword argument
'enable_legacy_on_interrupt_event'
Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a
fresh install already resolves to 0.0.43 and fails on the first message.
__init__ now forwards **kwargs upstream instead of restating the base signature.
That fixes the 500 without waiting on an upstream release, and makes base flags
reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not
be set by any CopilotKit user through this subclass.
Also constructs the intercepted-tool-call test double for real rather than via
object.__new__. That helper built an instance with no behavior flags set, so it
raised AttributeError out of the base dispatch path the moment ag-ui-langgraph
started reading one - 4 failures the pinned 0.0.42 lockfile currently hides.
Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235
passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed /
13 skipped (the two flag tests skip by design). End to end, two sequential
POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no
RUN_ERROR, where the published packages raise at endpoint.py:23.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
|
|
|
"""Tests for LangGraphAGUIAgent under the per-request clone().
|
|
|
|
|
|
|
|
|
|
``add_langgraph_fastapi_endpoint`` clones the agent on every request, so
|
|
|
|
|
``clone()`` is on the hot path for every self-hosted LangGraph deployment — a
|
|
|
|
|
failure there is a 500 on every request, not a startup error.
|
|
|
|
|
|
test(sdk-python): make the clone() kwargs guards falsifiable
Three assertions in test_agui_agent_clone.py passed whether or not the code
they name was correct. Each was found independently by a reviewer.
1. test_init_rejects_unknown_kwarg_end_to_end used a bare
pytest.raises(TypeError). The pre-fix closed 4-parameter signature raises
TypeError for a bogus kwarg too, from the subclass frame, so the assertion
could not distinguish "forwarded to the base, which rejected it" from
"rejected locally before forwarding" — reverting the **kwargs passthrough
left it green. Renamed to
test_init_rejects_unknown_kwarg_in_the_base_not_locally and given two
origin checks: match= on the BASE class's __init__ qualname (a closed
signature reports LangGraphAGUIAgent.__init__, not LangGraphAgent.__init__)
plus an assertion that the innermost traceback frame is the subclass's own
super().__init__ call site rather than the test module's construction call.
The match= also fixes the second half of the finding: an unrelated
TypeError on this path (the base's clone() re-raises construction failures
as TypeError) can no longer satisfy it.
2. test_clone_carries_upstream_flags asserted only the opted-out direction, so
it also passed in a world where emit_raw_events was forced False for
everyone — the inverse of the opt-out's intent. It now builds a default
clone as an in-test control and asserts both directions, rather than
borrowing the control from its sibling that the paired skipif silences at
the same time.
3. test_clone_resets_per_request_state mutated the source agent in setup and
then asserted the clone's attributes are None — true for any instance built
through __init__, so the setup was inert. Renamed to
test_clone_isolates_per_request_state_from_the_source and given the
assertions that make the setup load-bearing: the source keeps its own
run-local state across the clone. The two None assertions stay because they
are falsifiable (a copy.copy-style clone fails them), not decorative;
mutation M9 below proves it.
Also in this file:
- The 0.0.43 flag names lived in prose in test_clone_succeeds's docstring,
referenced by no test body, reading as exhaustive. They are now
CLONE_FORWARDED_FLAGS_0_0_43 and are asserted through the base __init__ spy,
so the list is live data; the comment says outright that it illustrates the
problem rather than staying exhaustive.
- test_init_forwards_unknown_kwargs_to_base (renamed
test_init_forwards_base_kwargs) now also asserts description and config
reach the base. The spy made it a two-line addition, and it closes the gap
that nothing pinned two of the four parameters __init__ names.
- The TextMessageContentEvent stub was built twice, once inline and once in a
nested closure; it is now one module-level _raw_event_message() helper. NOTE:
the graph stub is NOT duplicated in this file — _make_graph is already the
single local helper and all six call sites use it. The duplication a reviewer
flagged is across this file and test_intercepted_tool_call_events.py, which
is logged as deferred and untouched here.
Both skipif guards are kept deliberately. The declared floor is
ag-ui-langgraph >=0.0.42, where emit_raw_events does not exist, so skipping is
honest at the floor; test_init_forwards_base_kwargs is the guard that cannot
skip on any version.
Tests only. No change to copilotkit/langgraph_agui_agent.py, pyproject.toml or
any lockfile. Test count is unchanged at 7 in this file.
Mutation Evidence
Every added or changed assertion was mutation-tested: the guarded behavior
was broken, RED confirmed, source restored, GREEN confirmed. Source mutations
were applied to copilotkit/langgraph_agui_agent.py; M9/M10 to the installed
ag_ui_langgraph/agent.py. Both files restored by checksum afterwards.
M1 **kwargs passthrough reverted to the closed 4-parameter signature —
the exact scenario the old assertion could not detect.
@0.0.42 (the declared floor, what CI installs): NEW file ->
2 failed, 3 passed, 2 skipped
(test_init_rejects_unknown_kwarg_in_the_base_not_locally FAILED:
"Expected regex: LangGraphAgent\.__init__\(\) got an unexpected
keyword argument 'not_a_real_flag_the_base_defines'
Actual message: LangGraphAGUIAgent.__init__() got an unexpected
keyword argument 'not_a_real_flag_the_base_defines'")
PRE-FIX body of the same test -> PASSED. That is the defect.
@0.0.43: NEW file -> 7 failed; PRE-FIX bare-raises body -> PASSED.
M2 description=None in the super() call -> test_init_forwards_base_kwargs
FAILED (1 failed, 6 passed).
M3 config=None in the super() call -> test_init_forwards_base_kwargs FAILED.
M4 name mangled on the way to the base -> test_init_forwards_base_kwargs
FAILED.
M5 graph not relayed -> 6 failed, 1 passed.
M6 emit_raw_events forced False for everyone -> test_init_forwards_upstream_
flags AND test_clone_carries_upstream_flags FAILED. The PRE-FIX one-sided
clone body PASSED under the same mutation. That is defect 2.
M7 user's emit_raw_events opt-out overwritten with True -> both flag tests
FAILED (the other direction).
M8 emit_interrupt_outcome filtered out of the passthrough ->
test_init_forwards_base_kwargs FAILED with KeyError. Proves the flag list
is live data, not a comment.
M9 base clone() replaced with copy.copy(self) ->
test_clone_isolates_per_request_state_from_the_source FAILED. Proves the
two None assertions are falsifiable and worth keeping.
M10 base clone() wipes the SOURCE's run scope ->
test_clone_isolates_per_request_state_from_the_source FAILED, while the
PRE-FIX tautology body PASSED. That is defect 3.
Verification
Full sdk-python suite, Python 3.12 uv venv, ruff format --check clean:
ag-ui-langgraph 0.0.43 -> 237 passed, 11 skipped
ag-ui-langgraph 0.0.42 -> 235 passed, 13 skipped
Identical to the pre-change baseline on both versions, so no test count
decreased and the 0.0.42 delta is still only the two honest emit_raw_events
skips. `ruff check` still reports the same pre-existing I001 it reported on
the committed file (no ruff config or ruff step exists for sdk-python), so it
is left untouched. git status clean apart from this one test file; neither
pyproject.toml nor any lockfile appears in the diff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 18:01:52 +02:00
|
|
|
``LangGraphAgent.clone()`` reconstructs via ``type(self)(...)`` and forwards
|
|
|
|
|
whatever behavior flags the base defines, so each of them is a keyword argument
|
|
|
|
|
this subclass must be able to receive — which is why ``__init__`` forwards
|
|
|
|
|
**kwargs upstream rather than restating the base signature.
|
2026-09-01 12:29:49 +02:00
|
|
|
|
|
|
|
|
These tests deliberately assert behavior rather than upstream specifics: no
|
|
|
|
|
enumerated flag names, no error-message text, no traceback locations. The
|
2026-09-02 16:22:30 +02:00
|
|
|
supported range is ``ag-ui-langgraph>=0.0.42``, and CI exercises the declared
|
|
|
|
|
floor 0.0.42, the 0.0.43 whose ``clone()`` forwards its flags unconditionally,
|
|
|
|
|
and the newest 0.0.44, so anything version-dependent is guarded rather than
|
|
|
|
|
assumed.
|
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone()
add_langgraph_fastapi_endpoint clones the agent on every request, and
LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base
class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only
signature, so ag-ui-langgraph 0.0.43 - which began forwarding
enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events -
made every request 500 on the documented LangGraph + FastAPI quickstart:
TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__
accepts (name, graph, description, config) as keyword arguments:
LangGraphAGUIAgent.__init__() got an unexpected keyword argument
'enable_legacy_on_interrupt_event'
Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a
fresh install already resolves to 0.0.43 and fails on the first message.
__init__ now forwards **kwargs upstream instead of restating the base signature.
That fixes the 500 without waiting on an upstream release, and makes base flags
reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not
be set by any CopilotKit user through this subclass.
Also constructs the intercepted-tool-call test double for real rather than via
object.__new__. That helper built an instance with no behavior flags set, so it
raised AttributeError out of the base dispatch path the moment ag-ui-langgraph
started reading one - 4 failures the pinned 0.0.42 lockfile currently hides.
Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235
passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed /
13 skipped (the two flag tests skip by design). End to end, two sequential
POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no
RUN_ERROR, where the published packages raise at endpoint.py:23.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import inspect
|
2026-09-01 12:29:49 +02:00
|
|
|
from unittest.mock import MagicMock
|
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone()
add_langgraph_fastapi_endpoint clones the agent on every request, and
LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base
class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only
signature, so ag-ui-langgraph 0.0.43 - which began forwarding
enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events -
made every request 500 on the documented LangGraph + FastAPI quickstart:
TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__
accepts (name, graph, description, config) as keyword arguments:
LangGraphAGUIAgent.__init__() got an unexpected keyword argument
'enable_legacy_on_interrupt_event'
Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a
fresh install already resolves to 0.0.43 and fails on the first message.
__init__ now forwards **kwargs upstream instead of restating the base signature.
That fixes the 500 without waiting on an upstream release, and makes base flags
reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not
be set by any CopilotKit user through this subclass.
Also constructs the intercepted-tool-call test double for real rather than via
object.__new__. That helper built an instance with no behavior flags set, so it
raised AttributeError out of the base dispatch path the moment ag-ui-langgraph
started reading one - 4 failures the pinned 0.0.42 lockfile currently hides.
Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235
passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed /
13 skipped (the two flag tests skip by design). End to end, two sequential
POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no
RUN_ERROR, where the published packages raise at endpoint.py:23.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
|
|
|
|
|
|
|
|
import pytest
|
test(sdk-python): guard the clone() kwargs passthrough on any base version
The previous commit's regression was not guarded by its own test suite on the
dependency version CI installs. sdk-python/pyproject.toml declared
ag-ui-langgraph >= 0.0.42 and poetry.lock pinned 0.0.42, but the flags
clone() forwards (enable_legacy_on_interrupt_event, emit_interrupt_outcome,
emit_raw_events) only exist in 0.0.43+. At 0.0.42 the two flag tests skipif-
skipped and the remaining three passed against the OLD closed signature too:
reverting the **kwargs fix still produced "11 passed, 2 skipped", so CI would
have gone green on a branch with the fix removed.
Four changes close that:
1. test_init_forwards_unknown_kwargs_to_base patches the base __init__ with a
recorder and asserts an arbitrary unknown kwarg reaches it. It observes the
passthrough itself rather than whichever flags the installed base happens to
define, so it cannot skip and it fails on ANY base version if the subclass
goes back to a closed signature. This is the guard that would have caught
the regression at the pinned 0.0.42.
2. test_init_rejects_unknown_kwarg_end_to_end asserts a bogus kwarg raises
TypeError with no spy in place — **kwargs must forward to super() and let it
reject typos, never swallow them.
3. The two emit_raw_events tests now assert the EFFECT (a piggy-backed
raw_event is stripped by _dispatch_event) instead of the attribute value.
An attribute assertion cannot fail if the flag stops being honored.
4. The ag-ui-langgraph floor moves to >=0.0.43 (regenerated poetry.lock) so
those behavioral tests stop skipping in CI at all.
Also drops a dead `graph.get_state = MagicMock()` from the local _make_graph;
nothing in the exercised paths reads it (MagicMock auto-creates it anyway).
Verification
Red-green, fix reverted to the closed 4-kwarg signature:
ag-ui-langgraph 0.0.42 -> 1 failed, 4 passed, 2 skipped
(test_init_forwards_unknown_kwargs_to_base FAILED — previously this
state was fully green, which is the hole being closed)
ag-ui-langgraph 0.0.43 -> 6 failed, 1 passed
Fix restored, full sdk-python suite:
0.0.43 (Python 3.12 uv venv) -> 237 passed, 11 skipped
0.0.43 (poetry install --with dev, 3.13) -> 237 passed, 11 skipped
0.0.42 (Python 3.12 uv venv) -> 235 passed, 13 skipped
The only delta between 0.0.42 and 0.0.43 is the 2 emit_raw_events skips, so
the floor bump is the sole thing forcing 0.0.43.
`poetry lock` is idempotent on the committed lock (CI runs it before install)
and `poetry check --lock` exits 0. `ruff format` leaves the test file
unchanged. uv.lock is byte-identical (it locks only the dev group and
contains no ag-ui-langgraph entry; CI uses poetry, not uv).
Call-Site Enumeration
_make_graph (tests/test_agui_agent_clone.py) — CHANGED (get_state line
removed). 8 call sites, all in this file (the other test modules each have
their own local mock-graph builder). Assumption still holds: the removed
line only pre-set an attribute MagicMock creates on demand; both full
suites are green.
test_init_forwards_unknown_kwargs_to_base, test_init_rejects_unknown_kwarg_
end_to_end — ADDED. No references outside pytest collection.
test_init_forwards_upstream_flags, test_clone_carries_upstream_flags —
CHANGED bodies only. No references outside pytest collection.
BASE_INIT_PARAMS / `import inspect` — UNCHANGED and still live; both skipif
markers still read them. Kept deliberately: with the floor at >=0.0.43 they
can no longer fire on a supported install, and the version-independent
guard above cannot skip regardless, so they only protect a stale local env.
`patch`, `EventType`, `TextMessageContentEvent` — ADDED imports, used only in
this file. `MagicMock` still used by _make_graph.
ag-ui-langgraph constraint (sdk-python/pyproject.toml) — CHANGED. Consumers
in-repo: examples/integrations/langgraph-fastapi/agent/pyproject.toml and
the langgraph-fastapi / langgraph-python Dockerfiles pin PUBLISHED
copilotkit (0.1.93/0.1.94) alongside their own ag-ui-langgraph pins
(0.0.41/0.0.37), so they do not resolve this constraint and are unaffected
today; when they next bump copilotkit past the release carrying this change
their own pins will need to move to >=0.0.43. examples/v1/travel and
examples/showcases/* depend on ag-ui-langgraph directly, not through
sdk-python. No in-repo target installs copilotkit from this path.
sdk-python/poetry.lock — REGENERATED. Read by .github/workflows/
test_unit-python-sdk.yml (`poetry lock && poetry install --with dev`);
resolves ag-ui-langgraph 0.0.43. metadata.python-versions stays
">=3.10,<3.15" and uv.lock's requires-python is untouched, so Python 3.10
and 3.11 support is unchanged. Beyond the ag-ui-langgraph bump and its
transitive langgraph >=0.6.0 floor (already satisfied by locked 1.1.10),
the regeneration also (a) rewrote the generator header 2.1.3 -> 2.3.2,
(b) added the previously unlocked [dependency-groups] dev packages
(pytest, pytest-asyncio, pluggy, iniconfig, backports-asyncio-runner) and
the corresponding main+dev group markers, and (c) normalized a few
specifier spellings (2023.03.6 -> 2023.3.6, 14.05.14 -> 14.5.14,
2.0.0b -> 2.0.0b0). All three are artifacts of re-resolving with current
Poetry rather than of the dependency change; none alter a resolved main
dependency version.
sdk-python/pyproject.toml is also read by .github/workflows/
publish-release.yml (build-python fires on any merged PR touching it).
Assumption still holds: detect-py-version-changes.sh gates on the version
field, which is unchanged at 0.1.95, so the lane reports "Nothing to
publish". Publishing this fix still needs a separate version bump.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 17:23:25 +02:00
|
|
|
from ag_ui.core import EventType, TextMessageContentEvent
|
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone()
add_langgraph_fastapi_endpoint clones the agent on every request, and
LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base
class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only
signature, so ag-ui-langgraph 0.0.43 - which began forwarding
enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events -
made every request 500 on the documented LangGraph + FastAPI quickstart:
TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__
accepts (name, graph, description, config) as keyword arguments:
LangGraphAGUIAgent.__init__() got an unexpected keyword argument
'enable_legacy_on_interrupt_event'
Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a
fresh install already resolves to 0.0.43 and fails on the first message.
__init__ now forwards **kwargs upstream instead of restating the base signature.
That fixes the 500 without waiting on an upstream release, and makes base flags
reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not
be set by any CopilotKit user through this subclass.
Also constructs the intercepted-tool-call test double for real rather than via
object.__new__. That helper built an instance with no behavior flags set, so it
raised AttributeError out of the base dispatch path the moment ag-ui-langgraph
started reading one - 4 failures the pinned 0.0.42 lockfile currently hides.
Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235
passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed /
13 skipped (the two flag tests skip by design). End to end, two sequential
POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no
RUN_ERROR, where the published packages raise at endpoint.py:23.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
|
|
|
from ag_ui_langgraph import LangGraphAgent as AGUIBase
|
|
|
|
|
|
|
|
|
|
from copilotkit.langgraph_agui_agent import LangGraphAGUIAgent
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
BASE_INIT_PARAMS = inspect.signature(AGUIBase.__init__).parameters
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _make_graph():
|
|
|
|
|
graph = MagicMock()
|
|
|
|
|
graph.nodes = {}
|
|
|
|
|
return graph
|
|
|
|
|
|
|
|
|
|
|
test(sdk-python): make the clone() kwargs guards falsifiable
Three assertions in test_agui_agent_clone.py passed whether or not the code
they name was correct. Each was found independently by a reviewer.
1. test_init_rejects_unknown_kwarg_end_to_end used a bare
pytest.raises(TypeError). The pre-fix closed 4-parameter signature raises
TypeError for a bogus kwarg too, from the subclass frame, so the assertion
could not distinguish "forwarded to the base, which rejected it" from
"rejected locally before forwarding" — reverting the **kwargs passthrough
left it green. Renamed to
test_init_rejects_unknown_kwarg_in_the_base_not_locally and given two
origin checks: match= on the BASE class's __init__ qualname (a closed
signature reports LangGraphAGUIAgent.__init__, not LangGraphAgent.__init__)
plus an assertion that the innermost traceback frame is the subclass's own
super().__init__ call site rather than the test module's construction call.
The match= also fixes the second half of the finding: an unrelated
TypeError on this path (the base's clone() re-raises construction failures
as TypeError) can no longer satisfy it.
2. test_clone_carries_upstream_flags asserted only the opted-out direction, so
it also passed in a world where emit_raw_events was forced False for
everyone — the inverse of the opt-out's intent. It now builds a default
clone as an in-test control and asserts both directions, rather than
borrowing the control from its sibling that the paired skipif silences at
the same time.
3. test_clone_resets_per_request_state mutated the source agent in setup and
then asserted the clone's attributes are None — true for any instance built
through __init__, so the setup was inert. Renamed to
test_clone_isolates_per_request_state_from_the_source and given the
assertions that make the setup load-bearing: the source keeps its own
run-local state across the clone. The two None assertions stay because they
are falsifiable (a copy.copy-style clone fails them), not decorative;
mutation M9 below proves it.
Also in this file:
- The 0.0.43 flag names lived in prose in test_clone_succeeds's docstring,
referenced by no test body, reading as exhaustive. They are now
CLONE_FORWARDED_FLAGS_0_0_43 and are asserted through the base __init__ spy,
so the list is live data; the comment says outright that it illustrates the
problem rather than staying exhaustive.
- test_init_forwards_unknown_kwargs_to_base (renamed
test_init_forwards_base_kwargs) now also asserts description and config
reach the base. The spy made it a two-line addition, and it closes the gap
that nothing pinned two of the four parameters __init__ names.
- The TextMessageContentEvent stub was built twice, once inline and once in a
nested closure; it is now one module-level _raw_event_message() helper. NOTE:
the graph stub is NOT duplicated in this file — _make_graph is already the
single local helper and all six call sites use it. The duplication a reviewer
flagged is across this file and test_intercepted_tool_call_events.py, which
is logged as deferred and untouched here.
Both skipif guards are kept deliberately. The declared floor is
ag-ui-langgraph >=0.0.42, where emit_raw_events does not exist, so skipping is
honest at the floor; test_init_forwards_base_kwargs is the guard that cannot
skip on any version.
Tests only. No change to copilotkit/langgraph_agui_agent.py, pyproject.toml or
any lockfile. Test count is unchanged at 7 in this file.
Mutation Evidence
Every added or changed assertion was mutation-tested: the guarded behavior
was broken, RED confirmed, source restored, GREEN confirmed. Source mutations
were applied to copilotkit/langgraph_agui_agent.py; M9/M10 to the installed
ag_ui_langgraph/agent.py. Both files restored by checksum afterwards.
M1 **kwargs passthrough reverted to the closed 4-parameter signature —
the exact scenario the old assertion could not detect.
@0.0.42 (the declared floor, what CI installs): NEW file ->
2 failed, 3 passed, 2 skipped
(test_init_rejects_unknown_kwarg_in_the_base_not_locally FAILED:
"Expected regex: LangGraphAgent\.__init__\(\) got an unexpected
keyword argument 'not_a_real_flag_the_base_defines'
Actual message: LangGraphAGUIAgent.__init__() got an unexpected
keyword argument 'not_a_real_flag_the_base_defines'")
PRE-FIX body of the same test -> PASSED. That is the defect.
@0.0.43: NEW file -> 7 failed; PRE-FIX bare-raises body -> PASSED.
M2 description=None in the super() call -> test_init_forwards_base_kwargs
FAILED (1 failed, 6 passed).
M3 config=None in the super() call -> test_init_forwards_base_kwargs FAILED.
M4 name mangled on the way to the base -> test_init_forwards_base_kwargs
FAILED.
M5 graph not relayed -> 6 failed, 1 passed.
M6 emit_raw_events forced False for everyone -> test_init_forwards_upstream_
flags AND test_clone_carries_upstream_flags FAILED. The PRE-FIX one-sided
clone body PASSED under the same mutation. That is defect 2.
M7 user's emit_raw_events opt-out overwritten with True -> both flag tests
FAILED (the other direction).
M8 emit_interrupt_outcome filtered out of the passthrough ->
test_init_forwards_base_kwargs FAILED with KeyError. Proves the flag list
is live data, not a comment.
M9 base clone() replaced with copy.copy(self) ->
test_clone_isolates_per_request_state_from_the_source FAILED. Proves the
two None assertions are falsifiable and worth keeping.
M10 base clone() wipes the SOURCE's run scope ->
test_clone_isolates_per_request_state_from_the_source FAILED, while the
PRE-FIX tautology body PASSED. That is defect 3.
Verification
Full sdk-python suite, Python 3.12 uv venv, ruff format --check clean:
ag-ui-langgraph 0.0.43 -> 237 passed, 11 skipped
ag-ui-langgraph 0.0.42 -> 235 passed, 13 skipped
Identical to the pre-change baseline on both versions, so no test count
decreased and the 0.0.42 delta is still only the two honest emit_raw_events
skips. `ruff check` still reports the same pre-existing I001 it reported on
the committed file (no ruff config or ruff step exists for sdk-python), so it
is left untouched. git status clean apart from this one test file; neither
pyproject.toml nor any lockfile appears in the diff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 18:01:52 +02:00
|
|
|
def _raw_event_message():
|
|
|
|
|
"""A content event carrying the piggy-backed raw_event ``emit_raw_events`` gates."""
|
|
|
|
|
return TextMessageContentEvent(
|
|
|
|
|
type=EventType.TEXT_MESSAGE_CONTENT,
|
|
|
|
|
message_id="msg-1",
|
|
|
|
|
delta="hi",
|
|
|
|
|
raw_event={"event": "on_chat_model_stream"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone()
add_langgraph_fastapi_endpoint clones the agent on every request, and
LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base
class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only
signature, so ag-ui-langgraph 0.0.43 - which began forwarding
enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events -
made every request 500 on the documented LangGraph + FastAPI quickstart:
TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__
accepts (name, graph, description, config) as keyword arguments:
LangGraphAGUIAgent.__init__() got an unexpected keyword argument
'enable_legacy_on_interrupt_event'
Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a
fresh install already resolves to 0.0.43 and fails on the first message.
__init__ now forwards **kwargs upstream instead of restating the base signature.
That fixes the 500 without waiting on an upstream release, and makes base flags
reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not
be set by any CopilotKit user through this subclass.
Also constructs the intercepted-tool-call test double for real rather than via
object.__new__. That helper built an instance with no behavior flags set, so it
raised AttributeError out of the base dispatch path the moment ag-ui-langgraph
started reading one - 4 failures the pinned 0.0.42 lockfile currently hides.
Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235
passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed /
13 skipped (the two flag tests skip by design). End to end, two sequential
POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no
RUN_ERROR, where the published packages raise at endpoint.py:23.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
|
|
|
def test_clone_succeeds():
|
|
|
|
|
"""clone() must not raise, whatever flags the installed base forwards.
|
|
|
|
|
|
2026-09-01 12:29:49 +02:00
|
|
|
A closed signature rejects them, and since the endpoint clones per request
|
|
|
|
|
that is a 500 on every request.
|
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone()
add_langgraph_fastapi_endpoint clones the agent on every request, and
LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base
class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only
signature, so ag-ui-langgraph 0.0.43 - which began forwarding
enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events -
made every request 500 on the documented LangGraph + FastAPI quickstart:
TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__
accepts (name, graph, description, config) as keyword arguments:
LangGraphAGUIAgent.__init__() got an unexpected keyword argument
'enable_legacy_on_interrupt_event'
Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a
fresh install already resolves to 0.0.43 and fails on the first message.
__init__ now forwards **kwargs upstream instead of restating the base signature.
That fixes the 500 without waiting on an upstream release, and makes base flags
reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not
be set by any CopilotKit user through this subclass.
Also constructs the intercepted-tool-call test double for real rather than via
object.__new__. That helper built an instance with no behavior flags set, so it
raised AttributeError out of the base dispatch path the moment ag-ui-langgraph
started reading one - 4 failures the pinned 0.0.42 lockfile currently hides.
Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235
passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed /
13 skipped (the two flag tests skip by design). End to end, two sequential
POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no
RUN_ERROR, where the published packages raise at endpoint.py:23.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
|
|
|
"""
|
|
|
|
|
agent = LangGraphAGUIAgent(name="test", graph=_make_graph())
|
|
|
|
|
cloned = agent.clone()
|
|
|
|
|
assert isinstance(cloned, LangGraphAGUIAgent)
|
|
|
|
|
assert cloned is not agent
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_clone_preserves_copilotkit_state_namespace():
|
|
|
|
|
"""The copilotkit schema key must survive the per-request clone."""
|
|
|
|
|
agent = LangGraphAGUIAgent(name="test", graph=_make_graph())
|
|
|
|
|
cloned = agent.clone()
|
|
|
|
|
assert "copilotkit" in cloned.constant_schema_keys
|
|
|
|
|
assert cloned.constant_schema_keys.count("copilotkit") == 1
|
|
|
|
|
|
|
|
|
|
|
test(sdk-python): make the clone() kwargs guards falsifiable
Three assertions in test_agui_agent_clone.py passed whether or not the code
they name was correct. Each was found independently by a reviewer.
1. test_init_rejects_unknown_kwarg_end_to_end used a bare
pytest.raises(TypeError). The pre-fix closed 4-parameter signature raises
TypeError for a bogus kwarg too, from the subclass frame, so the assertion
could not distinguish "forwarded to the base, which rejected it" from
"rejected locally before forwarding" — reverting the **kwargs passthrough
left it green. Renamed to
test_init_rejects_unknown_kwarg_in_the_base_not_locally and given two
origin checks: match= on the BASE class's __init__ qualname (a closed
signature reports LangGraphAGUIAgent.__init__, not LangGraphAgent.__init__)
plus an assertion that the innermost traceback frame is the subclass's own
super().__init__ call site rather than the test module's construction call.
The match= also fixes the second half of the finding: an unrelated
TypeError on this path (the base's clone() re-raises construction failures
as TypeError) can no longer satisfy it.
2. test_clone_carries_upstream_flags asserted only the opted-out direction, so
it also passed in a world where emit_raw_events was forced False for
everyone — the inverse of the opt-out's intent. It now builds a default
clone as an in-test control and asserts both directions, rather than
borrowing the control from its sibling that the paired skipif silences at
the same time.
3. test_clone_resets_per_request_state mutated the source agent in setup and
then asserted the clone's attributes are None — true for any instance built
through __init__, so the setup was inert. Renamed to
test_clone_isolates_per_request_state_from_the_source and given the
assertions that make the setup load-bearing: the source keeps its own
run-local state across the clone. The two None assertions stay because they
are falsifiable (a copy.copy-style clone fails them), not decorative;
mutation M9 below proves it.
Also in this file:
- The 0.0.43 flag names lived in prose in test_clone_succeeds's docstring,
referenced by no test body, reading as exhaustive. They are now
CLONE_FORWARDED_FLAGS_0_0_43 and are asserted through the base __init__ spy,
so the list is live data; the comment says outright that it illustrates the
problem rather than staying exhaustive.
- test_init_forwards_unknown_kwargs_to_base (renamed
test_init_forwards_base_kwargs) now also asserts description and config
reach the base. The spy made it a two-line addition, and it closes the gap
that nothing pinned two of the four parameters __init__ names.
- The TextMessageContentEvent stub was built twice, once inline and once in a
nested closure; it is now one module-level _raw_event_message() helper. NOTE:
the graph stub is NOT duplicated in this file — _make_graph is already the
single local helper and all six call sites use it. The duplication a reviewer
flagged is across this file and test_intercepted_tool_call_events.py, which
is logged as deferred and untouched here.
Both skipif guards are kept deliberately. The declared floor is
ag-ui-langgraph >=0.0.42, where emit_raw_events does not exist, so skipping is
honest at the floor; test_init_forwards_base_kwargs is the guard that cannot
skip on any version.
Tests only. No change to copilotkit/langgraph_agui_agent.py, pyproject.toml or
any lockfile. Test count is unchanged at 7 in this file.
Mutation Evidence
Every added or changed assertion was mutation-tested: the guarded behavior
was broken, RED confirmed, source restored, GREEN confirmed. Source mutations
were applied to copilotkit/langgraph_agui_agent.py; M9/M10 to the installed
ag_ui_langgraph/agent.py. Both files restored by checksum afterwards.
M1 **kwargs passthrough reverted to the closed 4-parameter signature —
the exact scenario the old assertion could not detect.
@0.0.42 (the declared floor, what CI installs): NEW file ->
2 failed, 3 passed, 2 skipped
(test_init_rejects_unknown_kwarg_in_the_base_not_locally FAILED:
"Expected regex: LangGraphAgent\.__init__\(\) got an unexpected
keyword argument 'not_a_real_flag_the_base_defines'
Actual message: LangGraphAGUIAgent.__init__() got an unexpected
keyword argument 'not_a_real_flag_the_base_defines'")
PRE-FIX body of the same test -> PASSED. That is the defect.
@0.0.43: NEW file -> 7 failed; PRE-FIX bare-raises body -> PASSED.
M2 description=None in the super() call -> test_init_forwards_base_kwargs
FAILED (1 failed, 6 passed).
M3 config=None in the super() call -> test_init_forwards_base_kwargs FAILED.
M4 name mangled on the way to the base -> test_init_forwards_base_kwargs
FAILED.
M5 graph not relayed -> 6 failed, 1 passed.
M6 emit_raw_events forced False for everyone -> test_init_forwards_upstream_
flags AND test_clone_carries_upstream_flags FAILED. The PRE-FIX one-sided
clone body PASSED under the same mutation. That is defect 2.
M7 user's emit_raw_events opt-out overwritten with True -> both flag tests
FAILED (the other direction).
M8 emit_interrupt_outcome filtered out of the passthrough ->
test_init_forwards_base_kwargs FAILED with KeyError. Proves the flag list
is live data, not a comment.
M9 base clone() replaced with copy.copy(self) ->
test_clone_isolates_per_request_state_from_the_source FAILED. Proves the
two None assertions are falsifiable and worth keeping.
M10 base clone() wipes the SOURCE's run scope ->
test_clone_isolates_per_request_state_from_the_source FAILED, while the
PRE-FIX tautology body PASSED. That is defect 3.
Verification
Full sdk-python suite, Python 3.12 uv venv, ruff format --check clean:
ag-ui-langgraph 0.0.43 -> 237 passed, 11 skipped
ag-ui-langgraph 0.0.42 -> 235 passed, 13 skipped
Identical to the pre-change baseline on both versions, so no test count
decreased and the 0.0.42 delta is still only the two honest emit_raw_events
skips. `ruff check` still reports the same pre-existing I001 it reported on
the committed file (no ruff config or ruff step exists for sdk-python), so it
is left untouched. git status clean apart from this one test file; neither
pyproject.toml nor any lockfile appears in the diff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 18:01:52 +02:00
|
|
|
def test_clone_isolates_per_request_state_from_the_source():
|
|
|
|
|
"""The clone starts with a fresh run scope and the source keeps its own.
|
|
|
|
|
|
|
|
|
|
The endpoint holds one long-lived template agent and clones it per request,
|
|
|
|
|
so both halves are observable. The clone must not inherit the template's
|
|
|
|
|
run-local state — a ``copy.copy``-style clone would hand request N+1 the
|
|
|
|
|
state of request N — and cloning must not disturb the template either, or
|
|
|
|
|
the template stops being a clean starting point for every later request.
|
|
|
|
|
"""
|
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone()
add_langgraph_fastapi_endpoint clones the agent on every request, and
LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base
class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only
signature, so ag-ui-langgraph 0.0.43 - which began forwarding
enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events -
made every request 500 on the documented LangGraph + FastAPI quickstart:
TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__
accepts (name, graph, description, config) as keyword arguments:
LangGraphAGUIAgent.__init__() got an unexpected keyword argument
'enable_legacy_on_interrupt_event'
Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a
fresh install already resolves to 0.0.43 and fails on the first message.
__init__ now forwards **kwargs upstream instead of restating the base signature.
That fixes the 500 without waiting on an upstream release, and makes base flags
reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not
be set by any CopilotKit user through this subclass.
Also constructs the intercepted-tool-call test double for real rather than via
object.__new__. That helper built an instance with no behavior flags set, so it
raised AttributeError out of the base dispatch path the moment ag-ui-langgraph
started reading one - 4 failures the pinned 0.0.42 lockfile currently hides.
Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235
passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed /
13 skipped (the two flag tests skip by design). End to end, two sequential
POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no
RUN_ERROR, where the published packages raise at endpoint.py:23.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
|
|
|
agent = LangGraphAGUIAgent(name="test", graph=_make_graph())
|
|
|
|
|
agent.active_run = {"id": "run-1"}
|
|
|
|
|
agent._copilotkit_runtime_payload = {"actions": [], "context": []}
|
|
|
|
|
|
|
|
|
|
cloned = agent.clone()
|
|
|
|
|
|
|
|
|
|
assert cloned.active_run is None
|
|
|
|
|
assert cloned._copilotkit_runtime_payload is None
|
test(sdk-python): make the clone() kwargs guards falsifiable
Three assertions in test_agui_agent_clone.py passed whether or not the code
they name was correct. Each was found independently by a reviewer.
1. test_init_rejects_unknown_kwarg_end_to_end used a bare
pytest.raises(TypeError). The pre-fix closed 4-parameter signature raises
TypeError for a bogus kwarg too, from the subclass frame, so the assertion
could not distinguish "forwarded to the base, which rejected it" from
"rejected locally before forwarding" — reverting the **kwargs passthrough
left it green. Renamed to
test_init_rejects_unknown_kwarg_in_the_base_not_locally and given two
origin checks: match= on the BASE class's __init__ qualname (a closed
signature reports LangGraphAGUIAgent.__init__, not LangGraphAgent.__init__)
plus an assertion that the innermost traceback frame is the subclass's own
super().__init__ call site rather than the test module's construction call.
The match= also fixes the second half of the finding: an unrelated
TypeError on this path (the base's clone() re-raises construction failures
as TypeError) can no longer satisfy it.
2. test_clone_carries_upstream_flags asserted only the opted-out direction, so
it also passed in a world where emit_raw_events was forced False for
everyone — the inverse of the opt-out's intent. It now builds a default
clone as an in-test control and asserts both directions, rather than
borrowing the control from its sibling that the paired skipif silences at
the same time.
3. test_clone_resets_per_request_state mutated the source agent in setup and
then asserted the clone's attributes are None — true for any instance built
through __init__, so the setup was inert. Renamed to
test_clone_isolates_per_request_state_from_the_source and given the
assertions that make the setup load-bearing: the source keeps its own
run-local state across the clone. The two None assertions stay because they
are falsifiable (a copy.copy-style clone fails them), not decorative;
mutation M9 below proves it.
Also in this file:
- The 0.0.43 flag names lived in prose in test_clone_succeeds's docstring,
referenced by no test body, reading as exhaustive. They are now
CLONE_FORWARDED_FLAGS_0_0_43 and are asserted through the base __init__ spy,
so the list is live data; the comment says outright that it illustrates the
problem rather than staying exhaustive.
- test_init_forwards_unknown_kwargs_to_base (renamed
test_init_forwards_base_kwargs) now also asserts description and config
reach the base. The spy made it a two-line addition, and it closes the gap
that nothing pinned two of the four parameters __init__ names.
- The TextMessageContentEvent stub was built twice, once inline and once in a
nested closure; it is now one module-level _raw_event_message() helper. NOTE:
the graph stub is NOT duplicated in this file — _make_graph is already the
single local helper and all six call sites use it. The duplication a reviewer
flagged is across this file and test_intercepted_tool_call_events.py, which
is logged as deferred and untouched here.
Both skipif guards are kept deliberately. The declared floor is
ag-ui-langgraph >=0.0.42, where emit_raw_events does not exist, so skipping is
honest at the floor; test_init_forwards_base_kwargs is the guard that cannot
skip on any version.
Tests only. No change to copilotkit/langgraph_agui_agent.py, pyproject.toml or
any lockfile. Test count is unchanged at 7 in this file.
Mutation Evidence
Every added or changed assertion was mutation-tested: the guarded behavior
was broken, RED confirmed, source restored, GREEN confirmed. Source mutations
were applied to copilotkit/langgraph_agui_agent.py; M9/M10 to the installed
ag_ui_langgraph/agent.py. Both files restored by checksum afterwards.
M1 **kwargs passthrough reverted to the closed 4-parameter signature —
the exact scenario the old assertion could not detect.
@0.0.42 (the declared floor, what CI installs): NEW file ->
2 failed, 3 passed, 2 skipped
(test_init_rejects_unknown_kwarg_in_the_base_not_locally FAILED:
"Expected regex: LangGraphAgent\.__init__\(\) got an unexpected
keyword argument 'not_a_real_flag_the_base_defines'
Actual message: LangGraphAGUIAgent.__init__() got an unexpected
keyword argument 'not_a_real_flag_the_base_defines'")
PRE-FIX body of the same test -> PASSED. That is the defect.
@0.0.43: NEW file -> 7 failed; PRE-FIX bare-raises body -> PASSED.
M2 description=None in the super() call -> test_init_forwards_base_kwargs
FAILED (1 failed, 6 passed).
M3 config=None in the super() call -> test_init_forwards_base_kwargs FAILED.
M4 name mangled on the way to the base -> test_init_forwards_base_kwargs
FAILED.
M5 graph not relayed -> 6 failed, 1 passed.
M6 emit_raw_events forced False for everyone -> test_init_forwards_upstream_
flags AND test_clone_carries_upstream_flags FAILED. The PRE-FIX one-sided
clone body PASSED under the same mutation. That is defect 2.
M7 user's emit_raw_events opt-out overwritten with True -> both flag tests
FAILED (the other direction).
M8 emit_interrupt_outcome filtered out of the passthrough ->
test_init_forwards_base_kwargs FAILED with KeyError. Proves the flag list
is live data, not a comment.
M9 base clone() replaced with copy.copy(self) ->
test_clone_isolates_per_request_state_from_the_source FAILED. Proves the
two None assertions are falsifiable and worth keeping.
M10 base clone() wipes the SOURCE's run scope ->
test_clone_isolates_per_request_state_from_the_source FAILED, while the
PRE-FIX tautology body PASSED. That is defect 3.
Verification
Full sdk-python suite, Python 3.12 uv venv, ruff format --check clean:
ag-ui-langgraph 0.0.43 -> 237 passed, 11 skipped
ag-ui-langgraph 0.0.42 -> 235 passed, 13 skipped
Identical to the pre-change baseline on both versions, so no test count
decreased and the 0.0.42 delta is still only the two honest emit_raw_events
skips. `ruff check` still reports the same pre-existing I001 it reported on
the committed file (no ruff config or ruff step exists for sdk-python), so it
is left untouched. git status clean apart from this one test file; neither
pyproject.toml nor any lockfile appears in the diff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 18:01:52 +02:00
|
|
|
assert agent.active_run == {"id": "run-1"}
|
|
|
|
|
assert agent._copilotkit_runtime_payload == {"actions": [], "context": []}
|
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone()
add_langgraph_fastapi_endpoint clones the agent on every request, and
LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base
class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only
signature, so ag-ui-langgraph 0.0.43 - which began forwarding
enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events -
made every request 500 on the documented LangGraph + FastAPI quickstart:
TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__
accepts (name, graph, description, config) as keyword arguments:
LangGraphAGUIAgent.__init__() got an unexpected keyword argument
'enable_legacy_on_interrupt_event'
Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a
fresh install already resolves to 0.0.43 and fails on the first message.
__init__ now forwards **kwargs upstream instead of restating the base signature.
That fixes the 500 without waiting on an upstream release, and makes base flags
reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not
be set by any CopilotKit user through this subclass.
Also constructs the intercepted-tool-call test double for real rather than via
object.__new__. That helper built an instance with no behavior flags set, so it
raised AttributeError out of the base dispatch path the moment ag-ui-langgraph
started reading one - 4 failures the pinned 0.0.42 lockfile currently hides.
Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235
passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed /
13 skipped (the two flag tests skip by design). End to end, two sequential
POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no
RUN_ERROR, where the published packages raise at endpoint.py:23.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
|
|
|
"emit_raw_events" not in BASE_INIT_PARAMS,
|
|
|
|
|
reason="installed ag-ui-langgraph predates emit_raw_events",
|
|
|
|
|
)
|
2026-09-01 12:29:49 +02:00
|
|
|
def test_clone_carries_a_non_default_upstream_option():
|
|
|
|
|
"""A non-default base option must be settable here, and survive the clone.
|
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone()
add_langgraph_fastapi_endpoint clones the agent on every request, and
LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base
class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only
signature, so ag-ui-langgraph 0.0.43 - which began forwarding
enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events -
made every request 500 on the documented LangGraph + FastAPI quickstart:
TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__
accepts (name, graph, description, config) as keyword arguments:
LangGraphAGUIAgent.__init__() got an unexpected keyword argument
'enable_legacy_on_interrupt_event'
Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a
fresh install already resolves to 0.0.43 and fails on the first message.
__init__ now forwards **kwargs upstream instead of restating the base signature.
That fixes the 500 without waiting on an upstream release, and makes base flags
reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not
be set by any CopilotKit user through this subclass.
Also constructs the intercepted-tool-call test double for real rather than via
object.__new__. That helper built an instance with no behavior flags set, so it
raised AttributeError out of the base dispatch path the moment ag-ui-langgraph
started reading one - 4 failures the pinned 0.0.42 lockfile currently hides.
Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235
passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed /
13 skipped (the two flag tests skip by design). End to end, two sequential
POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no
RUN_ERROR, where the published packages raise at endpoint.py:23.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
|
|
|
|
2026-09-01 12:29:49 +02:00
|
|
|
Without the passthrough, ``emit_raw_events=False`` — the OSS-607 payload
|
test(sdk-python): guard the clone() kwargs passthrough on any base version
The previous commit's regression was not guarded by its own test suite on the
dependency version CI installs. sdk-python/pyproject.toml declared
ag-ui-langgraph >= 0.0.42 and poetry.lock pinned 0.0.42, but the flags
clone() forwards (enable_legacy_on_interrupt_event, emit_interrupt_outcome,
emit_raw_events) only exist in 0.0.43+. At 0.0.42 the two flag tests skipif-
skipped and the remaining three passed against the OLD closed signature too:
reverting the **kwargs fix still produced "11 passed, 2 skipped", so CI would
have gone green on a branch with the fix removed.
Four changes close that:
1. test_init_forwards_unknown_kwargs_to_base patches the base __init__ with a
recorder and asserts an arbitrary unknown kwarg reaches it. It observes the
passthrough itself rather than whichever flags the installed base happens to
define, so it cannot skip and it fails on ANY base version if the subclass
goes back to a closed signature. This is the guard that would have caught
the regression at the pinned 0.0.42.
2. test_init_rejects_unknown_kwarg_end_to_end asserts a bogus kwarg raises
TypeError with no spy in place — **kwargs must forward to super() and let it
reject typos, never swallow them.
3. The two emit_raw_events tests now assert the EFFECT (a piggy-backed
raw_event is stripped by _dispatch_event) instead of the attribute value.
An attribute assertion cannot fail if the flag stops being honored.
4. The ag-ui-langgraph floor moves to >=0.0.43 (regenerated poetry.lock) so
those behavioral tests stop skipping in CI at all.
Also drops a dead `graph.get_state = MagicMock()` from the local _make_graph;
nothing in the exercised paths reads it (MagicMock auto-creates it anyway).
Verification
Red-green, fix reverted to the closed 4-kwarg signature:
ag-ui-langgraph 0.0.42 -> 1 failed, 4 passed, 2 skipped
(test_init_forwards_unknown_kwargs_to_base FAILED — previously this
state was fully green, which is the hole being closed)
ag-ui-langgraph 0.0.43 -> 6 failed, 1 passed
Fix restored, full sdk-python suite:
0.0.43 (Python 3.12 uv venv) -> 237 passed, 11 skipped
0.0.43 (poetry install --with dev, 3.13) -> 237 passed, 11 skipped
0.0.42 (Python 3.12 uv venv) -> 235 passed, 13 skipped
The only delta between 0.0.42 and 0.0.43 is the 2 emit_raw_events skips, so
the floor bump is the sole thing forcing 0.0.43.
`poetry lock` is idempotent on the committed lock (CI runs it before install)
and `poetry check --lock` exits 0. `ruff format` leaves the test file
unchanged. uv.lock is byte-identical (it locks only the dev group and
contains no ag-ui-langgraph entry; CI uses poetry, not uv).
Call-Site Enumeration
_make_graph (tests/test_agui_agent_clone.py) — CHANGED (get_state line
removed). 8 call sites, all in this file (the other test modules each have
their own local mock-graph builder). Assumption still holds: the removed
line only pre-set an attribute MagicMock creates on demand; both full
suites are green.
test_init_forwards_unknown_kwargs_to_base, test_init_rejects_unknown_kwarg_
end_to_end — ADDED. No references outside pytest collection.
test_init_forwards_upstream_flags, test_clone_carries_upstream_flags —
CHANGED bodies only. No references outside pytest collection.
BASE_INIT_PARAMS / `import inspect` — UNCHANGED and still live; both skipif
markers still read them. Kept deliberately: with the floor at >=0.0.43 they
can no longer fire on a supported install, and the version-independent
guard above cannot skip regardless, so they only protect a stale local env.
`patch`, `EventType`, `TextMessageContentEvent` — ADDED imports, used only in
this file. `MagicMock` still used by _make_graph.
ag-ui-langgraph constraint (sdk-python/pyproject.toml) — CHANGED. Consumers
in-repo: examples/integrations/langgraph-fastapi/agent/pyproject.toml and
the langgraph-fastapi / langgraph-python Dockerfiles pin PUBLISHED
copilotkit (0.1.93/0.1.94) alongside their own ag-ui-langgraph pins
(0.0.41/0.0.37), so they do not resolve this constraint and are unaffected
today; when they next bump copilotkit past the release carrying this change
their own pins will need to move to >=0.0.43. examples/v1/travel and
examples/showcases/* depend on ag-ui-langgraph directly, not through
sdk-python. No in-repo target installs copilotkit from this path.
sdk-python/poetry.lock — REGENERATED. Read by .github/workflows/
test_unit-python-sdk.yml (`poetry lock && poetry install --with dev`);
resolves ag-ui-langgraph 0.0.43. metadata.python-versions stays
">=3.10,<3.15" and uv.lock's requires-python is untouched, so Python 3.10
and 3.11 support is unchanged. Beyond the ag-ui-langgraph bump and its
transitive langgraph >=0.6.0 floor (already satisfied by locked 1.1.10),
the regeneration also (a) rewrote the generator header 2.1.3 -> 2.3.2,
(b) added the previously unlocked [dependency-groups] dev packages
(pytest, pytest-asyncio, pluggy, iniconfig, backports-asyncio-runner) and
the corresponding main+dev group markers, and (c) normalized a few
specifier spellings (2023.03.6 -> 2023.3.6, 14.05.14 -> 14.5.14,
2.0.0b -> 2.0.0b0). All three are artifacts of re-resolving with current
Poetry rather than of the dependency change; none alter a resolved main
dependency version.
sdk-python/pyproject.toml is also read by .github/workflows/
publish-release.yml (build-python fires on any merged PR touching it).
Assumption still holds: detect-py-version-changes.sh gates on the version
field, which is unchanged at 0.1.95, so the lane reports "Nothing to
publish". Publishing this fix still needs a separate version bump.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 17:23:25 +02:00
|
|
|
opt-out — cannot be set by any CopilotKit user at all. Asserted through
|
2026-09-01 12:29:49 +02:00
|
|
|
``_dispatch_event`` rather than through the attribute: an attribute
|
|
|
|
|
assertion still passes if the option stops being applied to outgoing
|
|
|
|
|
events. Two-sided on purpose, since the opt-out direction alone also passes
|
|
|
|
|
in a world where raw events are dropped for everybody.
|
test(sdk-python): make the clone() kwargs guards falsifiable
Three assertions in test_agui_agent_clone.py passed whether or not the code
they name was correct. Each was found independently by a reviewer.
1. test_init_rejects_unknown_kwarg_end_to_end used a bare
pytest.raises(TypeError). The pre-fix closed 4-parameter signature raises
TypeError for a bogus kwarg too, from the subclass frame, so the assertion
could not distinguish "forwarded to the base, which rejected it" from
"rejected locally before forwarding" — reverting the **kwargs passthrough
left it green. Renamed to
test_init_rejects_unknown_kwarg_in_the_base_not_locally and given two
origin checks: match= on the BASE class's __init__ qualname (a closed
signature reports LangGraphAGUIAgent.__init__, not LangGraphAgent.__init__)
plus an assertion that the innermost traceback frame is the subclass's own
super().__init__ call site rather than the test module's construction call.
The match= also fixes the second half of the finding: an unrelated
TypeError on this path (the base's clone() re-raises construction failures
as TypeError) can no longer satisfy it.
2. test_clone_carries_upstream_flags asserted only the opted-out direction, so
it also passed in a world where emit_raw_events was forced False for
everyone — the inverse of the opt-out's intent. It now builds a default
clone as an in-test control and asserts both directions, rather than
borrowing the control from its sibling that the paired skipif silences at
the same time.
3. test_clone_resets_per_request_state mutated the source agent in setup and
then asserted the clone's attributes are None — true for any instance built
through __init__, so the setup was inert. Renamed to
test_clone_isolates_per_request_state_from_the_source and given the
assertions that make the setup load-bearing: the source keeps its own
run-local state across the clone. The two None assertions stay because they
are falsifiable (a copy.copy-style clone fails them), not decorative;
mutation M9 below proves it.
Also in this file:
- The 0.0.43 flag names lived in prose in test_clone_succeeds's docstring,
referenced by no test body, reading as exhaustive. They are now
CLONE_FORWARDED_FLAGS_0_0_43 and are asserted through the base __init__ spy,
so the list is live data; the comment says outright that it illustrates the
problem rather than staying exhaustive.
- test_init_forwards_unknown_kwargs_to_base (renamed
test_init_forwards_base_kwargs) now also asserts description and config
reach the base. The spy made it a two-line addition, and it closes the gap
that nothing pinned two of the four parameters __init__ names.
- The TextMessageContentEvent stub was built twice, once inline and once in a
nested closure; it is now one module-level _raw_event_message() helper. NOTE:
the graph stub is NOT duplicated in this file — _make_graph is already the
single local helper and all six call sites use it. The duplication a reviewer
flagged is across this file and test_intercepted_tool_call_events.py, which
is logged as deferred and untouched here.
Both skipif guards are kept deliberately. The declared floor is
ag-ui-langgraph >=0.0.42, where emit_raw_events does not exist, so skipping is
honest at the floor; test_init_forwards_base_kwargs is the guard that cannot
skip on any version.
Tests only. No change to copilotkit/langgraph_agui_agent.py, pyproject.toml or
any lockfile. Test count is unchanged at 7 in this file.
Mutation Evidence
Every added or changed assertion was mutation-tested: the guarded behavior
was broken, RED confirmed, source restored, GREEN confirmed. Source mutations
were applied to copilotkit/langgraph_agui_agent.py; M9/M10 to the installed
ag_ui_langgraph/agent.py. Both files restored by checksum afterwards.
M1 **kwargs passthrough reverted to the closed 4-parameter signature —
the exact scenario the old assertion could not detect.
@0.0.42 (the declared floor, what CI installs): NEW file ->
2 failed, 3 passed, 2 skipped
(test_init_rejects_unknown_kwarg_in_the_base_not_locally FAILED:
"Expected regex: LangGraphAgent\.__init__\(\) got an unexpected
keyword argument 'not_a_real_flag_the_base_defines'
Actual message: LangGraphAGUIAgent.__init__() got an unexpected
keyword argument 'not_a_real_flag_the_base_defines'")
PRE-FIX body of the same test -> PASSED. That is the defect.
@0.0.43: NEW file -> 7 failed; PRE-FIX bare-raises body -> PASSED.
M2 description=None in the super() call -> test_init_forwards_base_kwargs
FAILED (1 failed, 6 passed).
M3 config=None in the super() call -> test_init_forwards_base_kwargs FAILED.
M4 name mangled on the way to the base -> test_init_forwards_base_kwargs
FAILED.
M5 graph not relayed -> 6 failed, 1 passed.
M6 emit_raw_events forced False for everyone -> test_init_forwards_upstream_
flags AND test_clone_carries_upstream_flags FAILED. The PRE-FIX one-sided
clone body PASSED under the same mutation. That is defect 2.
M7 user's emit_raw_events opt-out overwritten with True -> both flag tests
FAILED (the other direction).
M8 emit_interrupt_outcome filtered out of the passthrough ->
test_init_forwards_base_kwargs FAILED with KeyError. Proves the flag list
is live data, not a comment.
M9 base clone() replaced with copy.copy(self) ->
test_clone_isolates_per_request_state_from_the_source FAILED. Proves the
two None assertions are falsifiable and worth keeping.
M10 base clone() wipes the SOURCE's run scope ->
test_clone_isolates_per_request_state_from_the_source FAILED, while the
PRE-FIX tautology body PASSED. That is defect 3.
Verification
Full sdk-python suite, Python 3.12 uv venv, ruff format --check clean:
ag-ui-langgraph 0.0.43 -> 237 passed, 11 skipped
ag-ui-langgraph 0.0.42 -> 235 passed, 13 skipped
Identical to the pre-change baseline on both versions, so no test count
decreased and the 0.0.42 delta is still only the two honest emit_raw_events
skips. `ruff check` still reports the same pre-existing I001 it reported on
the committed file (no ruff config or ruff step exists for sdk-python), so it
is left untouched. git status clean apart from this one test file; neither
pyproject.toml nor any lockfile appears in the diff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 18:01:52 +02:00
|
|
|
"""
|
|
|
|
|
default_clone = LangGraphAGUIAgent(name="test", graph=_make_graph()).clone()
|
|
|
|
|
opted_out_clone = LangGraphAGUIAgent(
|
|
|
|
|
name="test", graph=_make_graph(), emit_raw_events=False
|
|
|
|
|
).clone()
|
|
|
|
|
|
|
|
|
|
assert default_clone._dispatch_event(_raw_event_message()).raw_event is not None
|
|
|
|
|
assert opted_out_clone._dispatch_event(_raw_event_message()).raw_event is None
|