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>
## 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
## 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
## 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.
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>
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
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>
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
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.
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.
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.
@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.
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.
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>
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.
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.
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).
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.
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).
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.
Forwarded headers (e.g. X-AIMock-Context) were dropped before reaching the
wire in two cases. First, install_httpx_hook only attached to the immediate
client, missing an httpx client nested one or more ._client hops deep; it now
walks the ._client chain (bounded) to find the object that carries
event_hooks. Second, a sync def hook installed on an httpx.AsyncClient was
invoked as a coroutine and never awaited, silently dropping headers; the hook
is now async def for AsyncClient and sync def for Client. Async detection
prefers isinstance against the real httpx classes and falls back to an EXACT
"AsyncClient" MRO class-name match (not startswith("Async"), which would
misclassify a sync client whose MRO includes an Async*-named base).
Add integration and edge-case coverage for the new
_extract_forwarded_headers_from_config flow: wrapper-dict and raw x-*
sources, context > configurable precedence, mixed-case key normalization,
RuntimeError early-return clears stale ContextVar, exception path clears
stale ContextVar, None and empty-config fallbacks, and a sync/async
parity check that both call paths run the extraction.
Extract incoming x-* headers from LangGraph's runtime config and republish
them via the forwarded-headers ContextVar so the httpx hook can attach
them to outbound provider requests. Apply documented precedence
(context > configurable, wrapper-dict > raw x-*) by processing sources
in order with first-write-wins and lowercasing keys at insertion so
mixed-case headers do not silently overwrite each other downstream.
Always clear the ContextVar on early exits so stale headers from a prior
request never leak into the next: explicit set_forwarded_headers({}) on
the RuntimeError no-active-runnable path and on the generic exception
fallback. The happy path already overwrites the ContextVar
unconditionally, even with an empty dict.
The change also installs the httpx event hook once per chat-model client
via a module-level set keyed by id(client), so models reused across
requests pick up fresh per-request headers without re-hooking.
The new test_emit_tool_call_optional_id.py uses async test methods
decorated with @pytest.mark.asyncio, but pytest-asyncio was missing
from dev dependencies — causing all 11 async tests to fail in CI
across all Python versions (3.10–3.14).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The if (agent.headers) guard in configureAgentForRequest silently
skipped header forwarding when agent.headers was undefined (the
default for LangGraphAgent). This meant x-aimock-context, x-test-id,
and other x-* headers were never forwarded to agent backends.
Also wires install_httpx_hook in the Python SDK middleware so
forwarded headers propagate to outgoing LLM API calls.
Closes the gap documented in PR #4773 spec as out-of-scope.