7 Commits

Author SHA1 Message Date
Maxim 836e7c786f ci(sdk-python): add an ag-ui-langgraph 0.0.43 regression leg
The matrix covered the declared floor (0.0.42) and the newest release
(0.0.44) but skipped 0.0.43 — the only still-supported version that
reproduces the failure this PR fixes.

0.0.43's `LangGraphAgent.clone()` passes its three behavior flags to
`type(self)(...)` unconditionally, so a subclass with a closed signature
raises TypeError on the default construction path — a 500 on every
request, since the FastAPI endpoint clones per request. 0.0.44's
`clone()` is signature-aware and omits default-valued flags a subclass
cannot accept, which means the clone tests pass on 0.0.44 even with the
`**kwargs` passthrough removed. The `emit_raw_events=False` test still
guards option reachability there, but nothing in the matrix reproduced
the default-construction 500 itself.

Verified locally against 0.0.43: all four clone tests pass with the
passthrough and all four fail without it, so the leg is a real guard.

One representative Python (3.12) via `matrix.include` rather than a
third full column — the flag forwarding it exercises is not
version-specific, so this adds one leg, not five. Installed in the leg
rather than declared, so the runtime floor stays at 0.0.42 and the
effective LangGraph floor stays at >=0.3.25 for consumers.

Also corrects the `test_agui_agent_clone.py` module docstring, which
claimed CI exercised 0.0.42 and 0.0.43 while the workflow installed
0.0.42 and 0.0.44.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 16:22:30 +02:00
Maxim 801b8e8bba refactor(sdk-python): narrow the clone-compat PR to the shim and its behavior
Review feedback from contextablemark on #6592.

- Restores the ag-ui-langgraph >=0.0.42 runtime floor and reverts the poetry
  lockfile. Raising the floor to 0.0.43 for test coverage alone would also have
  raised the effective LangGraph floor from >=0.3.25 to >=0.6.0 for every
  consumer, which is a real cost for no user-facing benefit.
- Covers 0.0.42 and 0.0.44 in the python-sdk CI matrix instead, pinned after the
  lock resolve so the declared floor is untouched and only the installed version
  varies per leg.
- Drops the tests coupled to upstream specifics: the enumerated forwarded-flag
  list, the base-__init__ spy, and the unknown-kwarg test that asserted on the
  TypeError message text and the raising traceback frame. What remains is
  behavior the subclass owns: cloning succeeds, the copilotkit schema namespace
  survives, per-request state is isolated in both directions, and a non-default
  upstream option survives the clone (guarded, since the floor predates it).

The passthrough itself and the test-helper fix are unchanged.
2026-09-01 12:29:49 +02:00
Maxim c8cbebe3db test(sdk-python): make the forwarded-flag list actually live data
The comment on CLONE_FORWARDED_FLAGS_0_0_43 claimed the names were "asserted
so the list is live data instead of prose that quietly goes stale". Four
reviewers pointed out that the claim was false: the only assertion over the
list runs inside test_init_forwards_base_kwargs, which spies with a **kwargs
stand-in that accepts any keyword. That is why the same test can assert
not_a_real_flag_the_base_defines and pass. The loop could never fail, so the
list was prose after all.

Adds test_forwarded_flag_names_exist_on_the_base, which checks the names
against the real base signature, and rewrites the comment to say which
assertion does what. A flag the base renames or drops now fails loudly instead
of leaving the list describing a version nobody runs.

Verified: simulating upstream drift by renaming one entry fails exactly the new
test (1 failed, 7 passed); restored, the suite is 238 passed / 11 skipped.

Not changed, deliberately: the two `is None` isolation assertions a reviewer
called unfalsifiable. Re-tested by replacing the base clone() with copy.copy —
test_clone_isolates_per_request_state_from_the_source FAILS under that mutant,
so those assertions do guard something and removing them would drop real
coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 12:27:21 +02:00
Maxim 2fbbca40c6 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-09-01 12:27:18 +02:00
Maxim cdfd4e4ce0 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-09-01 12:27:16 +02:00
github-actions[bot] a3f16891ce style: auto-fix formatting 2026-09-01 12:27:16 +02:00
Maxim a1c598dc14 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-09-01 12:27:16 +02:00