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.
The AG-UI dispatcher's ManuallyEmitToolCall handler rejected non-dict,
non-string args with CopilotKitMisuseError, but all three emitters
(JS, Python LangGraph, Python CrewAI) accept any JSON-serializable
value. This mismatch caused JS-emitted list/number args to crash the
Python dispatcher.
Replace the strict isinstance(dict, str) check with a None guard and
rely on the existing json.dumps try/except for serializability.
Call-site enumeration:
- langgraph_agui_agent.py:129 — changed from isinstance check to None guard
- test_emit_tool_call_optional_id.py — updated test_missing_args_raises match,
test_non_serializable_args_raises match, converted test_list_args_raises and
test_int_args_raises from negative to positive tests
- No other call sites reference the removed isinstance pattern
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove no-op `try/except CancelledError: raise` around asyncio.shield
in copilotkit_emit_message (the shield handles cancellation on its own).
Remove unused CopilotKitError and CopilotKitMisuseError imports from
sdk.py (already re-exported via __init__.py).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The split queue_put calls introduced interleaving risk (6 yield points
vs 2) and the end_attempted flag was set before the END dispatch,
causing the compensating END to be skipped when END itself failed.
- CrewAI: restore single batched queue_put(start, args, end) call;
compensating END is now unconditional on batch failure
- AG-UI agent: rename end_attempted → end_dispatched, set after
successful END dispatch so compensation fires for all failure modes
- Tests: rewrite CrewAI compensating tests for batch semantics,
add test_failure_on_end_emits_compensating_end for AG-UI agent
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace JS error.message mutation with wrapped Error + cause chain
(safe for frozen errors, shared references, non-Error throwables)
- Rename dispatched_end → end_attempted, set before END dispatch to
prevent duplicate TOOL_CALL_END when END partially flushes before throwing
- Switch JS randomId() (ck-prefixed) to randomUUID() for cross-SDK parity
with Python's str(uuid.uuid4())
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wrap JS dispatchCustomEvent in try/catch that enriches error messages
with tool name and ID for debuggability
- Apply asyncio.shield to copilotkit_emit_message's post-dispatch sleep
to match copilotkit_emit_tool_call's behavior under task cancellation
- Reorder validation in LangGraph Python and JS to name → toolCallId →
args, matching CrewAI's order (cheap checks before serialization)
- Narrow AG-UI dispatcher's except clause around json.dumps from
Exception to (TypeError, ValueError), matching sibling SDK variants
- Add CancelledError propagation and warning-log tests for the shielded
sleep path
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Re-raise CancelledError after logging in langgraph copilotkit_emit_tool_call
to honor asyncio cancellation contract (was silently un-cancelling tasks)
- Move dispatched_end flag to after ToolCallEndEvent dispatch in AG-UI agent
so compensating END fires when END itself throws
- Add dispatched_end tracking to CrewAI variant to prevent double-END on
partial failure
- Add JSON.stringify(args) validation in JS SDK matching Python parity
- Add 4 CrewAI compensating-END tests and 1 JS serializability test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address code review findings across all three SDK variants:
- Shield asyncio.sleep(0.02) with asyncio.shield() so task cancellation
doesn't prevent returning the tool_call_id after dispatch
- Revert args validation to original permissiveness (JS: undefined-only
check, Python: no isinstance check) to avoid breaking existing callers
- Add upfront json.dumps() serializability check in Python variants
- Fix compensating TOOL_CALL_END double-emit by tracking dispatched_end
- Add compensating action_execution_end to CrewAI variant (queue_put is
non-atomic)
- Use exc_info=True in compensating-END error logging
- Export all exception types from copilotkit package root (__init__.py)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add missing @returns tag to JS copilotkitEmitToolCall JSDoc, and add
4 tests for the AG-UI compensating TOOL_CALL_END error-recovery path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address code review findings:
- Wrap AG-UI tool call dispatch in try/except with compensating
TOOL_CALL_END to prevent clients hanging on partial emission
- Reject non-dict/non-str args at the dispatch layer (lists, ints, None)
- Guard against None event value before calling .get()
- Fix docstring examples that reuse variable names (won't compile)
- Introduce CopilotKitError base class; all exceptions now inherit from
it; CopilotKitMisuseError inherits from both CopilotKitError and
ValueError
- Add missing validation tests for name and args across LangGraph and
CrewAI Python variants, plus AG-UI dispatch edge cases
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address code review findings: stop mislabeling dispatch errors as
CopilotKitMisuseError in JS (let them propagate naturally), add
CopilotKitMisuseError(ValueError) to Python SDK, pre-serialize args
in AG-UI handler to prevent partial event emission, align whitespace
validation across all SDKs and the dispatch layer, tighten JS args
type to Record<string, unknown>, and add comprehensive negative tests
for AG-UI dispatch validation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Restore CopilotKitMisuseError for dispatch failures in JS (was bare Error)
- Rename JS options.id to options.toolCallId for cross-SDK naming parity
- Add name/args validation to Python LangGraph and CrewAI variants
- Add defensive field validation in AG-UI dispatch handler
- Add missing CrewAI whitespace-only ID test
- Add JS dispatch failure test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align JS whitespace-only ID rejection with Python (.trim()), show
returned ID in docstring examples, strengthen CrewAI test assertions
to verify event payloads structurally, and stop miscategorizing
dispatch errors as CopilotKitMisuseError (preserve original stack).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address review feedback on copilotkit_emit_tool_call:
- Add non-empty string validation for the tool call ID in all 3 SDKs
- Rename Python `id` param to `tool_call_id` to avoid shadowing the builtin
- Refactor JS 4th positional arg to options bag `{ id?: string }` for extensibility
- Document that the ID is also used as parentMessageId in AG-UI events
- Add JS tests for the new parameter (generated ID, custom ID, validation)
- Add Python validation tests (empty string, whitespace rejection)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allow callers to supply a custom tool call ID for correlation,
idempotency, and observability. Falls back to uuid4 when omitted.
Applied consistently across Python LangGraph, Python CrewAI, and JS SDK.
Also returns the tool call ID from all variants for downstream reference.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Matches the CopilotKit runtime's extractForwardableHeaders() which
already forwards all x-* prefixed headers. Enables any custom
x-* header to propagate from browser through AG-UI to LLM calls.
ContextVar-based ambient state + httpx event hook. Incoming
x-aimock-* headers from AG-UI requests are forwarded to outgoing
LLM API calls. Keys normalized to lowercase. Warning emitted when
install_httpx_hook receives an unrecognized client type.
Plain `poetry lock` (no --regenerate) was resolving pydantic-core 2.33.2,
which has no cp314 wheel and forces a Rust source build that fails on
PyO3 0.24 (capped at 3.13). Floor pin steers the resolver to 2.46.3
which ships cp314 wheels, so contributor PRs that bump deps and re-lock
keep CI green on 3.14.
The A2UI React renderer (packages/a2ui-renderer/src/react-renderer/a2ui-react/A2uiSurface.tsx:152)
always begins rendering at the component with id="root":
export const A2uiSurface: React.FC<{...}> = ({ surface }) => {
// The root component always has ID 'root' and base path '/'
return <DeferredChild surface={surface} id="root" basePath="/" />;
};
If no component has that ID, DeferredChild falls through to its loading-
shimmer placeholder, so the surface silently renders as an empty ~30px
rectangle regardless of how many other components are on the surface.
The generation guidelines shipped to the sub-LLM (in @copilotkit/shared
and copilotkit sdk-python) never stated this requirement. Fixed-schema
demos hard-code a component with id="root" in their JSON and work; dynamic
demos relied on the LLM guessing, which it sometimes did and sometimes
didn't. The failure mode is particularly nasty: no error, no warning,
just a loading spinner that never resolves.
Adds the requirement to COMPONENT ID RULES in both the TS and Python
guideline strings. Both strings are injected into the sub-LLM's context
by A2UICatalogContext (packages/react-core) and
copilotkit.a2ui.a2ui_prompt() respectively, so every A2UI-enabled app
picks it up automatically — no per-demo change needed.
Stacked on #4216, which restores the same instruction to the
langgraph-python-threads demo's tool docstring (belt-and-braces until
consumers update their shared package version).
ag-ui-protocol 0.1.15 added a validator requiring TextMessageContentEvent.delta
to have length >= 1. The test asserted that manually_emit_message with an empty
string still emits the full TEXT_MESSAGE_START/CONTENT/END sequence — that
behavior is no longer achievable at the protocol layer, and emitting empty
content events was never semantically useful. Removing rather than guarding
in code: the protocol should fail loudly on empty deltas, not silently drop
them in CopilotKit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Our branch commit 506a1e8e9 removed the unused 'import json' from
copilotkit/langgraph.py. Main PR #3784 subsequently added a dict-resume
path in copilotkit_interrupt that calls json.dumps(response) and re-added
the import.
The 3-way merge had no textual conflict (the lines around the import
didn't change on both sides), so git silently took our "delete" over
main's "unchanged." Result: json.dumps() called with json undefined.
Verified: sdk-python test suite now 74/74 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The main-merge commit (0e77affe1) removed sdk-python/copilotkit/langgraph_agent.py
as deprecated, but three test files still imported private helpers from it:
- test_emit_state_merge.py (_merge_emit_state)
- test_pydantic_state_serialization.py (LangGraphAgent, _serialize_state)
- test_sanitize_for_json.py (_sanitize_for_json)
Collection failed with ModuleNotFoundError, so the python-sdk unit job failed.
Behavior these tests covered now lives in the ag_ui_langgraph PyPI package
(external dep), not in this repo — so there is nothing to port.
Also fix test_emit_filtering.py::test_run_filters_none_events: replace
asyncio.get_event_loop().run_until_complete(...) with asyncio.run(...) —
get_event_loop() raises RuntimeError in Python 3.12 when no loop is current.
The failure was masked in CI because collection errored out before this test ran.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
- Widens the python dependency from `>=3.10,<3.13` to `>=3.10,<4`
- Adds Python 3.13 classifier to enable installation on newer Python
versions
Closes#3156