274 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 418f17c975 Revert "revert(sdk-python): restore the ag-ui-langgraph >=0.0.42 floor"
This reverts commit 6b30fb64ecc7dbbb31555569de3c8a9bc8f4570f.
2026-09-01 12:27:20 +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 830c53a449 revert(sdk-python): restore the ag-ui-langgraph >=0.0.42 floor
The floor bump to >=0.0.43 rested on a premise that does not hold. Six of
seven reviewers reported that this PR's regression was unguarded in CI,
reasoning from poetry.lock pinning 0.0.42. CI does not use that pin:

  .github/workflows/test_unit-python-sdk.yml
  run: poetry lock && poetry install --with dev

poetry lock regenerates the lockfile from scratch, so CI already resolved
0.0.43 under the original >=0.0.42 floor, the behavioral clone tests already
ran there, and a reverted fix would already have failed CI. The measurement
behind the claim was real but described a local install, not CI.

0.0.42 is code-compatible with the kwargs passthrough: the base takes the
four documented parameters, we pass four, and the only thing 0.0.42 lacks is
the optional behavior flags, whose absence merely skips two tests. So the
floor narrowed users' install range for no benefit, and forced a 129-line
poetry.lock regeneration (previously-unlocked dev-group closure) along with
it, since poetry requires lock/pyproject consistency.

Both files are restored byte-identical to main. The tests FIX-B1 added are
kept: the version-independent spy test is what actually guards the
passthrough, and it holds on any base version. Restoring the floor also makes
the skipif guards on the two flag tests live again rather than dead code.

Verified after the revert:
  0.0.43                -> 237 passed, 11 skipped
  0.0.42                -> 235 passed, 13 skipped
  0.0.42 + fix reverted -> 1 failed (test_init_forwards_unknown_kwargs_to_base)

A floor bump becomes justified when upstream ships the clone() fix as 0.0.44,
because that is the first release where third-party subclasses are safe. That
belongs in the release commit that consumes it, not here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 12:27:17 +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
1553126902 7b7e5cc510 test(sdk-python): cover orphan function_call stripping through the wrap path
Add two request-scoped regressions for #6676 beyond the unit tests:

- test_replayed_cancelled_turn_strips_orphan_function_call_blocks: the
  poisoned-checkpoint shape (run cancelled mid-turn, function_call blocks
  persisted with no ToolMessages and no intercept state) driven through
  wrap_model_call/awrap_model_call — the model must receive a history
  with no orphaned blocks and no unanswered tool_calls, which is what
  keeps langchain-openai from re-emitting them as Responses input items
  without a matching function_call_output.
- test_next_model_call_keeps_answered_function_call_blocks_on_restore:
  the legitimate resume path — restored frontend call with its synthetic
  result plus the answered backend call keep their function_call blocks,
  proving the sanitizer does not over-strip the orphan-handoff contract.
2026-08-29 22:22:48 +08:00
hata33 963f753f53 fix(sdk-python): strip orphan OpenAI Responses function_call content blocks
after_model strips intercepted frontend tool_calls from the AIMessage but
leaves the equivalent responses/v1 function_call content blocks in
message.content. When a run is cancelled mid-turn the partial turn persists
without a ToolMessage, and on replay langchain-openai serializes the
orphaned block into the Responses API input with no matching
function_call_output, so OpenAI rejects every subsequent turn with
400 "No tool output found for function call call_...".

Extend the checkpoint message sanitizer to treat function_call blocks
like the existing Anthropic tool_use handling: strip blocks whose call_id
is missing from msg.tool_calls, strip all of them when tool_calls is
empty, and strip the ones belonging to unanswered tool_calls.

Fixes #6676
2026-08-29 10:58:40 +08:00
copilotkit-qa-bot[bot] 4c5402a0aa chore(sdk-python): release 0.1.96 2026-08-26 15:12:51 -07:00
github-actions[bot] f2ce7fa52d style: auto-fix formatting 2026-08-19 15:06:19 +00:00
Benjamin Taylor d37fe9cbd2 test(sdk-python): revalidate the poetry lock and cover the partialjson parse path
Widening the partialjson constraint in #6123 invalidated poetry.lock's
content-hash, so a bare `poetry install` in sdk-python fails with
"pyproject.toml changed significantly since poetry.lock was last generated".
CI never saw it because test_unit-python-sdk.yml runs `poetry lock` first, and
publishing is unaffected because `poetry build` ignores the lock — but local
dev is blocked until someone relocks. Refreshed with Poetry 2.1.3 (the lock's
own generator) so the diff stays limited to the hash, and moved partialjson to
1.1.0 so CI exercises the version a fresh install now resolves. ag-ui-langgraph
is deliberately left at 0.0.42: a from-scratch resolve pulls 0.0.43, which
fails four intercepted-tool-call tests on a missing `emit_raw_events`.

The parse path had no coverage at all — disabling JSONParser.parse outright
left all 225 tests passing, because every partialjson failure mode degrades to
"no predicted state was emitted" behind the bare `except` in predict_state().
These tests pin the guarantees the `>=0.0.8,<2.0.0` range must keep, and are
version-agnostic about intermediate frames, which legitimately differ (1.1.0
preserves trailing whitespace mid-string where 0.0.8 dropped it).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 10:04:11 -05:00
Ben Taylor e0a2f00cca fix: relax partialjson version constraint in sdk-python (#6123)
## What does this PR do?

Relaxes the over-strict `partialjson` version constraint in
`sdk-python/pyproject.toml` so the CopilotKit Python SDK can consume
newer compatible `partialjson` releases.

**Change:** `sdk-python/pyproject.toml`
- Before: `partialjson = "^0.0.8"`
- After:  `partialjson = ">=0.0.8,<2.0.0"`

## Why

Poetry interprets `^0.0.8` on a `0.0.x` version as `>=0.0.8,<0.0.9`,
which effectively pins the SDK to **exactly** `0.0.8`. This blocks users
from receiving newer, compatible `partialjson` releases (currently
published: `0.0.9`, `0.1.0`, `1.0.0`, `1.1.0`). Relaxing the constraint
lets the SDK pick up fixes and features while still treating the next
major (`2.0.0`) as the breaking-change boundary.

I verified the API the SDK actually uses — `from partialjson.json_parser
import JSONParser` and `JSONParser().parse(...)` — is unchanged across
all published versions up to `1.1.0` (the `JSONParser.__init__` /
`parse` signatures are backward compatible, and the SDK call passes no
arguments). There is no breaking-API risk within the chosen range.

## Test plan

- [x] Validated `sdk-python/pyproject.toml` is well-formed:
`python3 -c "import tomllib,pathlib;
tomllib.load(pathlib.Path('sdk-python/pyproject.toml').open('rb'))"`
- [x] Grepped `sdk-python` for `partialjson` usages — only
`copilotkit/runloop.py` imports/uses it; API is stable across the
allowed range.
- [ ] `poetry lock` / `poetry install` in `sdk-python` resolves a
`partialjson` version within `>=0.0.8,<2.0.0`.

## Changed files

- `sdk-python/pyproject.toml` — single line changed (dependency range
only). No source, example, or lockfile changes.

## Checklist

- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] No documentation change required (dependency range only)
- [x] "Allow edits by maintainers" is checked

Relates to #4131
2026-08-18 09:56:22 -05:00
Ben Taylor e6d3629445 fix(sdk-python): emit tool call events for middleware-intercepted SDK Actions (#5372)
## Summary

Frontend SDK Actions intercepted by `CopilotKitMiddleware` could
disappear before the client received a tool-call lifecycle, leaving the
handler uncalled and the conversation without a tool result or follow-up
answer. The bridge now consumes the middleware's authoritative
intercepted-call state and publishes each call through the AG-UI adapter
at most once. The focused intercepted file has 8 tests and the manual
AG-UI file has 30 tests; the combined run is 38 tests, with the
supported-version matrix remaining CI-owned.

## Root cause

`CopilotKitMiddleware.after_model()` removes frontend calls before
LangChain's backend tool node and records those exact calls for
final-message restoration. The previous adapter fallback ignored that
state and reconstructed frontend ownership from `on_chat_model_end` plus
the run's action catalog. That duplicated classification, failed when an
action carried `function: None`, and never ran on Python 3.10 because
the async LangChain path emitted no model callbacks there.

## Changes

- `sdk-python/copilotkit/langgraph_agui_agent.py`
- publish middleware-classified calls from the existing chain-state
event path
- reuse one lifecycle materializer and the parent adapter's per-run ID
ledger
- remove raw model-end/action-schema reconstruction and the duplicate
logger assignment
- `sdk-python/tests/test_intercepted_tool_call_events.py`
- exercise the real `create_agent(..., tools=[])` middleware and AG-UI
path in streaming and non-streaming modes
- cover exactly-once identity, Python 3.10 restoration, multiple IDs,
metadata opt-out, backend negative space, and malformed action/state
shapes

## What is not changed

- backend tools still use LangChain's normal tool node
- `after_agent` still restores intercepted calls into the final
assistant snapshot
- tool-call emission metadata still controls streamed lifecycle
visibility
- explicit `copilotkit_emit_tool_call()` calls keep their existing
caller-controlled semantics

## Related PRs and Issues

Closes #4342.

## Test plan

- [x] Focused intercepted SDK Action regression file, 8 passed on Python
3.12. Covers the real `create_agent(..., tools=[])` path, streaming,
restoration, mixed ownership, chain metadata suppression, malformed
state, and action-shape isolation.
- [x] Existing manual tool-call materialization file, 30 passed on
Python 3.12.
- [x] Combined focused selection, 38 passed on Python 3.12, counted once
rather than adding a second 38-test total.
- [x] Ruff check and format check on both changed files, both passed.
- [ ] Python SDK CI matrix green on Python 3.10, 3.11, 3.12, 3.13, and
3.14
2026-08-17 12:58:37 -05:00
Ben Taylor cd0d38a073 fix(sdk-python): restore frontend tool history faithfully in LangGraph middleware (#5308)
## Description

Fixes the LangGraph middleware "lying" to the agent about frontend tool
execution (#4759). When a model turn calls a `useFrontendTool` handler,
the middleware strips the FE tool calls so the backend `ToolNode` only
runs calls it actually made, then rehydrates them afterward. Previously
the rehydration could leave the restored AI message and its tool history
inconsistent, so providers that require every tool call to have a
matching result would break, and the agent saw an incorrect picture of
what executed.

## Changes

- `_restore_intercepted_tool_call_history` rebuilds the intercepted FE
tool calls with synthetic `ToolMessage` results only for the restored
model history (so providers that require every `tool_call` to have a
result stay valid), and preserves the original AI message's `name`,
`additional_kwargs`, and `response_metadata` on restore.
- Tracks `original_tool_calls` in the CopilotKit private state so the
original call set is restored exactly.

## Testing

`uv run pytest tests/test_copilotkit_lg_middleware.py` — 68 passed,
including new cases asserting the restored AI message keeps its
tool-call ids, name, `additional_kwargs`, and `response_metadata`, and
that tool results line up with their calls.

Closes #4759

AI was used for assistance.
2026-08-17 12:54:26 -05:00
Cursor Agent 49bf4535a9 test(sdk-python): add checkpoint roundtrip coverage for orphan-handoff contract
Adds tests that exercise the full checkpoint roundtrip path:
- test_checkpoint_roundtrip_placeholder_replaced_by_real_result: Verifies
  _fix_messages_for_bedrock correctly replaces patch_orphan_tool_calls
  placeholder with real FE result
- test_checkpoint_roundtrip_multiple_fe_calls_with_placeholders: Multiple
  FE calls with placeholders all correctly resolved
- test_after_agent_leaves_fe_call_orphaned_for_checkpoint: Confirms
  after_agent does not persist synthetic ToolMessages, leaving FE calls
  as orphans for the real result to fill

These tests address reviewer feedback requesting coverage for the checkpoint
persist → add_messages merge → patch_orphan_tool_calls → real FE result path.

Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>
2026-08-16 18:12:11 +00:00
mvanhorn 467d1c5337 fix: implemented the remaining review fix:
Implemented the remaining review fix:
- Sync and async model restoration now operate on copied message lists, preventing synthetic results from entering checkpoint state.
- Removed the obsolete synthesis flag and ineffective result-ID filter.
- `after_agent` restores the frontend call as an orphan.
- Added sync/async regression coverage for request-scoped restoration.
[Middleware changes](/Users/mvanhorn/.osc/workspaces/CopilotKit-CopilotKit-pr5308/sdk-python/copilotkit/copilotkit_lg_middleware.py:450)
[Regression tests](/Users/mvanhorn/.osc/workspaces/CopilotKit-CopilotKit-pr5308/sdk-python/tests/test_copilotkit_lg_middleware.py:618)
```text
2026-08-13 09:39:14 -07:00
Rod Boev d9667bc5f1 test(sdk-python): cover intercepted event delivery boundaries 2026-08-12 12:39:39 -04:00
Rod Boev ca2818f6dc fix(sdk-python): deliver intercepted action events through the adapter 2026-08-12 12:22:12 -04:00
Rod Boev 9f77123df4 fix(sdk-python): route intercepted action events through adapter filtering 2026-08-12 12:10:16 -04:00
Benjamin Taylor 07855f3fc9 chore: release python sdk 0.1.95
Ships the four sdk-python fixes merged since 0.1.94 (2026-06-04), none of
which are in any published artifact — the newest upload of any kind is the
0.1.95a4 prerelease from 2026-06-19, which predates all of them.

Closes #6231

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 11:58:08 -05:00
Rod Boev 26bcbc8043 fix(sdk-python): keep intercepted tool call emission on the AG-UI adapter (#4342) 2026-07-27 07:33:18 -04:00
Rod Boev 1340fd806d fix(sdk-python): suppress duplicate streamed tool lifecycle events (#4342) 2026-07-27 07:28:19 -04:00
Rod Boev 3338c7f1e4 test(sdk-python): prove intercepted sdk action event emission (#4342) 2026-07-27 07:24:12 -04:00
Rod Boev 9fcaf34225 fix(sdk-python): emit tool call events for middleware-intercepted SDK Actions 2026-07-27 07:24:12 -04:00
godququ5-code bf5b1a0be4 fix: relax partialjson version constraint in sdk-python
Poetry's ^0.0.8 on a 0.0.x version resolves to >=0.0.8,<0.0.9,
pinning users to exactly 0.0.8 and blocking newer compatible
releases (0.0.9, 0.1.0, 1.0.0, 1.1.0). Relax to >=0.0.8,<2.0.0
so the SDK can pick up fixes/features while still gating on the
next major (2.0.0) as the breaking boundary.

The only SDK usage is partialjson.json_parser.JSONParser().parse(),
which is unchanged across all published versions up to 1.1.0.

Relates to #4131
2026-07-27 10:10:50 +03:00
Rod Boev a76d59ae0b fix(sdk-python): capture subgraph context from run input (#3886) 2026-07-26 16:42:46 -04:00
Matt Van Horn 9f847a2e47 fix(sdk-python): keep the frontend tool call orphaned in after_agent
Addresses both review concerns on the after_agent half.

after_agent no longer synthesizes a forwarded_to_frontend ToolMessage. That
synthetic result did not match _INTERRUPTED_PAT, so _fix_messages_for_bedrock
and langchain's patch_orphan_tool_calls treated it as a real second result and
skipped the reposition path, which on the Bedrock path could leave the real
frontend result non-adjacent and then stripped as unanswered. The restore helper
now takes synthesize_frontend_tool_results, which after_agent sets to False so
the call stays an orphan for the real result to fill, and any synthetic result
already in history is dropped by id.

The sync wrap_model_call now also runs _fix_messages_for_bedrock, matching
awrap_model_call, so the messages it injects go through the same dedup instead
of risking a duplicate tool_call id on the sync path.
2026-07-25 13:32:24 -07:00
Rod Boev fee7ec237c fix(sdk-python): bridge copilotkit context into LangGraph subgraphs 2026-07-24 16:19:13 -04:00
Rod Boev bb32138e15 fix(sdk-python): read copilotkit context from config when state is empty (subgraph support) 2026-07-24 16:13:05 -04:00
godququ5-code 44c43e4770 fix(python-sdk): fold app context into system prompt 2026-07-03 01:06:09 +03:00
Ran Shem Tov 186b007091 chore(sdk-python): require ag-ui-langgraph >=0.0.42
0.0.42 ships the single-arg A2UIToolParams API the middleware's a2ui_params
override relies on; pulls ag-ui-a2ui-toolkit 0.0.4 transitively. uv.lock is
unchanged — it locks only the dev/test toolchain, not the runtime deps.
2026-06-19 17:51:02 +02:00
Ran Shem Tov f13865a21f fix(sdk-python): declare ag-ui state channel and add a2ui_params host override
The middleware reads state["ag-ui"]["inject_a2ui_tool"] and
state["ag-ui"]["a2ui_schema"], but "ag-ui" was never declared on the
StateSchema, so create_agent's StateGraph dropped both keys before the
middleware ran — the generate_a2ui tool never injected and the catalog was
lost. Declare the "ag-ui" channel via StateSchema.__annotations__ so the flag
and catalog survive.

Also add an a2ui_params kwarg so a host can steer the auto-injected
generate_a2ui subagent (design/generation guidelines, catalog id, ...). The
middleware still injects the bound model and folds the registered catalog in,
but host-set values win.
2026-06-19 17:50:26 +02:00
Ran Shem Tov 6e9240accc chore(deps): revert @ag-ui/core,client to 0.0.53 (decouple from langgraph 0.0.41 bump)
@ag-ui/client 0.0.56 changed runHttpRequest to a thunk signature, breaking
@copilotkit/core's ProxiedCopilotRuntimeAgent. OSS-248 only needs
@ag-ui/langgraph 0.0.41; keep that, revert the unrelated core/client/protocol
'latest' bump. Adopting client 0.0.56 is a separate migration.
2026-06-09 16:59:50 +02:00
Ran Shem Tov cf7bcd67e5 chore(deps): use latest @ag-ui packages and langgraph integration
- @ag-ui/core, @ag-ui/client: 0.0.53 -> 0.0.56 across all packages
  (react-core, core, react-native, vue, runtime, angular, shared,
  web-inspector, agentcore-runner, demo-agents, sqlite-runner) + root
  pnpm override; shared's @ag-ui/core range floor -> >=0.0.56.
- @ag-ui/langgraph (runtime): 0.0.39 -> 0.0.40.
- ag-ui-protocol (sdk-python): >=0.1.15 -> >=0.1.19.
- .npmrc: exclude first-party @ag-ui/{core,client,encoder,proto} from the
  minimum-release-age gate so the freshly-published 0.0.56 set installs.
- Regenerate pnpm-lock.yaml + sdk-python/poetry.lock.
2026-06-09 12:14:12 +02:00
Ran Shem Tov 5bb32b9dd5 feat(a2ui): use new ag-ui single-arg A2UIToolParams API (OSS-248)
ag-ui-langgraph's tool factory now takes one A2UIToolParams object (model
inside) and folds composition_guide into the guidelines bag.

- sdk-js middleware: getA2UITools(params) + type A2UIToolParams import;
  pin @ag-ui/langgraph 0.0.40 (the JS release carrying the new API).
- sdk-python middleware: get_a2ui_tools(params) + guarded A2UIToolParams
  import; pin ag-ui-langgraph >=0.0.41 (the py release with the new API).
- Update the a2ui injection test skip-reason wording.
2026-06-09 12:13:51 +02:00
Ran Shem Tov c85c140f05 feat: update all dependencies to use latest a2ui implementation features 2026-06-08 12:09:20 +02:00
Matt Van Horn 850c74e4b8 fix(sdk-python): keep LangGraph middleware honest about frontend tool execution
The middleware strips FE tool calls so the backend ToolNode only executes
its own calls, then restores them. Restoration now rebuilds the intercepted
calls with synthetic ToolMessage results scoped to the restored model
history (providers that require a result per tool_call stay valid) and
preserves the original AI message's name, additional_kwargs, and
response_metadata. Tracks original_tool_calls in private state so the
original call set is restored exactly.

Closes #4759

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 09:02:34 -07:00
Ran Shem Tov 2b5d2e0113 chore: release python sdk 0.1.94 2026-06-04 19:39:44 +02:00
Ran Shem Tov 02ae7895e9 chore: restore copilotkit py version 2026-06-04 18:37:52 +02:00
Ran Shem Tov 3ca0f194b8 feat(sdk): gate auto-A2UI injection on injectA2UITool (opt-in)
The A2UI middleware (@ag-ui/a2ui-middleware) forwards injectA2UITool on
forwardedProps; ag-ui-langgraph surfaces it into agent state at
state["ag-ui"]["inject_a2ui_tool"]. The CopilotKit LangGraph middleware
(py + js) now reads that flag and only injects generate_a2ui when it is
truthy (opt-in), drops the runtime's render_a2ui so the model sees one
A2UI tool, and skips if the agent already defines generate_a2ui. The
catalog only binds surfaces; it is no longer the gate.

Reverts the earlier runtime-forward + context-channel approach.

Deps: ag-ui-langgraph>=0.0.38 (py), @ag-ui/langgraph 0.0.37 (sdk-js),
@ag-ui/a2ui-middleware 0.0.6 + @ag-ui/langgraph 0.0.37 (runtime);
.npmrc min-release-age exclude for @ag-ui/a2ui-middleware. Showcase
langgraph pins bumped to copilotkit==0.1.94a3 / sdk-js 1.59.3-alpha.3 /
@ag-ui/langgraph 0.0.37.
2026-06-04 18:37:03 +02:00
Ran Shem Tov 1bd944bd43 feat(sdk): source A2UI catalog wherever the frontend registered it
The auto-injected generate_a2ui tool now resolves the A2UI catalog from
both delivery paths: the CopilotKit runtime proxy (a copilotkit.context
entry) and the AG-UI native endpoint (state["ag-ui"].a2ui_schema). The
registered catalog id is extracted and bound to generated surfaces so
BYOC custom catalogs render their own components instead of the basic
catalog. Covers @copilotkit/sdk-js and the copilotkit Python SDK.
2026-06-04 18:36:12 +02:00
Ran Shem Tov ce4e4142b2 feat(sdk): auto-inject A2UI tool in CopilotKitMiddleware
Prebuilt agents get dynamic A2UI with no extra wiring — adding the
middleware is enough. When the frontend registers an A2UI catalog
(surfaced by the runtime into state["ag-ui"].a2ui_schema), the
middleware infers the agent's own model, advertises the generate_a2ui
tool in the model-call hook, and executes it in the tool-call hook.
No catalog → the tool is never advertised.

Covers both @copilotkit/sdk-js and the copilotkit Python SDK. Bumps
the A2UI tool-factory dependency to where get_a2ui_tools ships
(@ag-ui/langgraph 0.0.35, ag-ui-langgraph >=0.0.37).
2026-06-04 18:36:12 +02:00
Jordan Ritter ea5ea897c1 style(sdk-python): apply ruff format to test_agui_agent.py
main's static/quality format gate was red because this file was not
ruff-formatted (introduced by an earlier unrelated python commit).
Apply ruff 0.15.13 format to restore the gate.
2026-05-29 12:54:47 -07:00
Jordan Ritter e77856e6b7 chore(sdk-python): release v0.1.93 2026-05-28 21:15:00 -07:00
github-actions[bot] b2f10f6753 style: auto-fix formatting 2026-05-29 03:58:54 +00:00
Jordan Ritter 59eff59b7f fix(sdk-python): strip copilotkit_forwarded_headers from LLM prompt (App Context + expose_state)
langgraph-api auto-copies the entire config.configurable dict into runtime.context. That dict
carries the CopilotKit-internal transport key `copilotkit_forwarded_headers`, populated solely
to drive the httpx header-forwarding hook (it is not user-visible state). The middleware's
before_agent step then rendered runtime.context into the LLM prompt as an "App Context" system
message, and the expose_state path could surface the same key via the state note — either path
leaks transport headers into the prompt body and, under strict fixture matching (D6/aimock),
changes the request payload and breaks the match.

Fix: hard-exclude `copilotkit_forwarded_headers` from both render paths. The App Context
renderer strips the reserved key before serialization, and the expose_state allowlist applies
the same exclusion so an explicit allow cannot reintroduce the leak. The httpx hook is
unchanged, so the key still reaches aimock as an HTTP header — pure conveyance, no body
pollution.

Adds red-green unit coverage for both paths (App Context strip, expose_state default + allowlist).
2026-05-28 20:57:37 -07:00
Jordan Ritter 3110cf9b00 docs(sdk-python): document header_propagation module purpose and scope
Adds a module-level docstring at the top of header_propagation.py that explains in plain
English what the module does (forwards CopilotKit request-context x-* headers onto
outbound LLM/provider HTTP calls so downstream services like the aimock test server and
proxies can correlate the outbound call with the original inbound request), the precise
scope (only headers the application itself set via set_forwarded_headers; no request
bodies, cookies, user data, credentials, or telemetry), and the mechanics (walks the
._client chain to find the httpx client and attaches an async hook for AsyncClient or a
sync hook for Client). Also softens a few comments and docstrings whose wording could
read as surveillance language ("inject" / "installing an async hook ... silently dropped")
to neutral engineering phrasing ("attach" / "the forwarded headers would not be attached
to the outbound request") without losing any technical meaning or warnings. No executable
logic, signatures, conditionals, or string literals in code paths were changed; the diff
is comments and docstrings only.

In response to review feedback on PR #5088.
2026-05-28 14:39:07 -07:00