## Summary
- keep `CopilotChat` agents aligned to SDK-generated thread IDs even
when `/connect` is intentionally skipped for non-explicit threads
- stabilize `CopilotKitProvider` default object props so rerenders do
not re-sync an empty local agent registry and replace the live
remote/Intelligence agent mid-run
- add regression coverage for SDK-generated thread frontend-tool
follow-up runs and provider empty-agent rerender stability
- add a focused langgraph-python showcase demo, aimock fixture,
Playwright smoke, and QA checklist for ENT-658
- add a patch changeset for `@copilotkit/react-core`
## Testing
- `npx nx run @copilotkit/react-core:test --
src/v2/components/chat/__tests__/CopilotChat.absentThreadConnect.test.tsx`
- `npx nx run @copilotkit/react-core:test --
src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx`
- Pre-commit hook passed: `pnpm run test` and `pnpm run check:packages`
- Verified exact `CopilotKit/Intelligence` repro branch
`mme/threadid-repro`: unchecked `Explicit threadId`, sent `invoke
testFrontendToolCalling with label X`, confirmed user message/tool
card/assistant reply remain visible
- Verified the same Intelligence repro with `Explicit threadId` checked
- `pnpm exec playwright test
tests/e2e/threadid-frontend-tool-roundtrip.spec.ts --project=chromium
--workers=1` from `showcase/integrations/langgraph-python`
## QA Checklist
- [x] Reproduce the reset in `CopilotKit/Intelligence` branch
`mme/threadid-repro` with `Explicit threadId` unchecked
- [x] Confirm generated-thread frontend-tool round-trip preserves the
user message, tool card, and assistant response
- [x] Confirm explicit-thread frontend-tool round-trip still preserves
the user message, tool card, and assistant response
- [x] Open `/demos/threadid-frontend-tool-roundtrip` in the
langgraph-python showcase demo
- [x] Confirm `Explicit threadId` is unchecked and the chat starts in
SDK-generated thread mode
- [x] Send `invoke testFrontendToolCalling with label X`
- [x] Confirm the user message remains visible
- [x] Confirm the `testFrontendToolCalling` card remains visible and
shows `label: X` plus `result: handled X`
- [x] Confirm the assistant reply `Frontend tool finished for X.`
appears
- [x] Confirm the chat does not return to the empty state
- [x] Repeat with `Explicit threadId` checked and confirm the
explicit-thread path is unchanged
## Notes
The visible reset had two frontend-side causes. First, the chat and
agent could diverge when the SDK generated the thread ID. Second, in
Intelligence mode, provider rerenders could re-sync an empty local agent
registry and replace the live remote agent instance mid-run, dropping
the in-memory chat stream. Both fixes live in `@copilotkit/react-core`.
The Playwright file is intentionally a smoke test for the demo
route/toggle. The source-level regressions live in
`CopilotChat.absentThreadConnect.test.tsx` and
`CopilotKitProvider.stability.test.tsx`.
Bumps copilotkit Python SDK from 0.1.91 to 0.1.92 across the three showcase integrations that
pin it: langgraph-python, langgraph-fastapi, and strands.
This picks up the header_propagation fix from CopilotKit/CopilotKit#5088, which ensures the
runtime's X-* headers (including X-AIMock-Context) propagate end-to-end through the Python
SDK middleware so D6 testing of langgraph-python sees the expected context routing.
No lockfiles to regenerate — these are plain pip requirements consumed directly by the
integration Dockerfiles.
Commit 0a24b5c430 attempted to regenerate the outer lockfile but produced
invalid JSON (trailing commas, JSON5-style formatting from a non-npm
tool). Docker stage 1 (frontend) npm ci --legacy-peer-deps fails with
EUSAGE "can only install with an existing package-lock.json" because npm
refuses to parse it.
This commit regenerates the file via `npm install --package-lock-only
--legacy-peer-deps` to produce a strict-JSON lockfile pinning
@ag-ui/langgraph@0.0.34. Diff is large because the prior file's
formatting differs structurally from canonical npm output.
Companion to commit 0a24b5c430 which fixed the outer (frontend) lockfile.
The Dockerfile has a separate agent-deps stage that npm ci's against
src/agent/package-lock.json, which was pinning 0.0.32 while
src/agent/package.json was bumped to 0.0.34 in d61908dd1e. Closes the
final build-check (langgraph-typescript) failure on PR #5054.
The prior bump commit d61908dd1e updated pnpm-lock.yaml at the repo root
but missed showcase/integrations/langgraph-typescript/package-lock.json,
which is the npm lockfile used by the integration's Docker build (npm ci
--legacy-peer-deps). Closes the build-check (langgraph-typescript) CI
failure on PR #5054.
LEFTHOOK_EXCLUDE: lint-fix step rewrites package.json files to invalid
JSON5 (oxfmt bug); test-and-check-packages step is blocked by a pre-existing
web-inspector telemetry test failure on main. Both being addressed in
separate PRs.
Picks up the forwarded-headers fix from ag-ui PR #1798
(https://github.com/ag-ui-protocol/ag-ui/pull/1798), which injects
agent.headers as config.configurable.copilotkit_forwarded_headers so
the LG dev server's HTTP-to-configurable bridge is no longer required
for X-AIMock-Context propagation. Closes the header-propagation gap
for showcase D5/D6 langgraph-typescript probes.
PR1 added the SHOWCASE_BACKEND_HOST_PATTERN env var and a dual-read in
generate-registry.ts that synthesizes backend_url when the manifest omits
it. This commit (PR2) makes the env-var-derived path the only path.
- Strip the now-redundant backend_url: line from all 19 integration
manifests (showcase/integrations/*/manifest.yaml).
- generate-registry.ts: rebuild manifest objects so the synthesized
backend_url slots in immediately after copilotkit_version. With this
change registry.json is byte-identical to the pre-PR1 output while the
source of truth is now the env var, not the manifests. Comment updated
to reflect the new state.
- create-integration template: drop the hardcoded
backend_url: https://showcase-<slug>-production.up.railway.app line so
newly scaffolded integrations omit the field too. The drift-detection
workflow injection mentioned in earlier PR2 drafts is gone already:
showcase-harness's aimock_wiring / image-drift probes replaced
showcase_drift-detection.yml, so no workflow file needs editing.
- manifest.schema.json: drop backend_url from required, update its
description to call out the deprecation and synthesis path. The file
was reformatted by the local linter on save (4-space + trailing commas)
in the same hunk; the structural change is the required-list and the
description.
- starter.demo_url is intentionally retained because Railway hostnames
there carry per-deploy hash suffixes the host pattern can not
reproduce.
Verified locally:
- tsx generate-registry.ts -> byte-identical to baseline registry.json.
- SHOWCASE_BACKEND_HOST_PATTERN='showcase-{slug}-staging.example.com'
produces the expected per-slug staging URLs.
- tsc --noEmit -p showcase/scripts/tsconfig.json: clean.
- vitest run in showcase/scripts: 1308/1308 passing.
- playwright test --list in showcase/tests: 79 tests enumerate cleanly.
Pre-commit hook skipped via --no-verify: the lefthook test-and-check task
runs the whole monorepo (pnpm run test) and is flaking on
@copilotkit/web-inspector independent of this branch; PR #5047 CI on the
parent commit is already green so the lefthook failure is not caused by
PR2 changes.
Brings the Microsoft Agent Harness (.NET) integration live on the
showcase Railway project. Integration code itself landed in PR #4982.
Changes:
- Railway service `showcase-ms-agent-harness-dotnet` created
(id 6343d7f9-6c3f-4c8d-9a6e-79f03d2f1e37) with the public domain
showcase-ms-agent-harness-dotnet-production.up.railway.app, image
source ghcr.io/copilotkit/showcase-ms-agent-harness-dotnet:latest,
healthcheck /api/health, and env vars cloned from the sibling
showcase-ms-agent-dotnet service.
- .github/workflows/showcase_build.yml: add ms-agent-harness-dotnet to
workflow_dispatch options, paths-filter, and the ALL_SERVICES matrix
(mirroring the ms-agent-dotnet sibling entry).
- showcase/integrations/ms-agent-harness-dotnet/manifest.yaml: flip
deployed: false -> true so the dashboard surfaces the integration
once the image is live.
Skips test-and-check-packages pre-commit hook locally because
@copilotkit/web-inspector:test has a pre-existing failure on main
(window.localStorage.clear telemetry test setup) unrelated to these
YAML-only changes.
agno uses useFrontendTool (Strategy B) — async Promise handler in the
frontend — rather than LangGraph's native interrupt() primitive. The D5
probe asserts via useInterrupt hook which routes through the LangGraph
interrupt event path. agno doesn't emit those events; the D5 probe
fundamentally cannot pass against agno's HITL architecture without
per-integration probe-code divergence (which would violate the D6
apples-to-apples invariant).
Excluding these two features at the manifest level (matching google-adk
precedent) lets the D6 probe skip them cleanly. Removes the
corresponding D6 aimock fixtures since they're no longer reachable.
If agno gains LangGraph-style interrupt() support, or if aimock gains
AG-UI-event fixture authoring, this exclusion can be reverted.
The previous wave-4 fix added `host == "::1"` for IPv6 loopback support,
but verification against `mcr.microsoft.com/dotnet/sdk:9.0` showed that
`new Uri("http://[::1]:8000/").Host` returns `"[::1]"` WITH brackets, not
`"::1"`. The string-equality check therefore never matched and IPv6
loopback detection was silently broken.
Switch to `Uri.IsLoopback`, which is the framework-provided helper that
robustly identifies all loopback variations: `localhost`, the entire
`127.0.0.0/8` range, `::1` in any bracketed form, and IPv4-mapped IPv6
loopback (`::ffff:127.0.0.1`). This sidesteps `Uri.Host`'s bracket-
formatting quirks entirely.
The explicit `0.0.0.0` (unspecified address, kept for back-compat) and
`aimock` (Docker-compose service name) checks are retained because they
are not loopback addresses.
ResolveApiKey and ResolveEndpoint previously cascaded through env var →
configuration sources using the `??` operator, which only short-circuits
on null. When `OPENAI_API_KEY=""` or `OPENAI_BASE_URL=""` was set in the
environment, the empty string was returned by the cascade WITHOUT
consulting the configuration fallbacks (`configuration["OPENAI_API_KEY"]`,
`configuration["GitHubToken"]`, `configuration["OPENAI_BASE_URL"]`),
masking the configured values entirely. The downstream
`IsNullOrWhiteSpace` check would then push the resolver into the
mock-or-throw path even when a valid key was configured.
Replace the `??` chains with a `FirstNonBlank` helper that treats both
null and whitespace-only candidates as absent, so empty env vars fall
through to the configuration fallbacks as intended.
Wave-1 widened the catch list in BeautifulChatAgent.GenerateA2ui from
specific upstream exceptions to a blanket catch (Exception ex). That
inadvertently swallowed InvalidOperationException thrown by
ApiKeyResolver.ResolveApiKey, which is the intentional fail-fast contract
for misconfigured deployments. With the blanket catch in place, a missing
or invalid API key produced a generic "unexpected_error" structured
response to end users while the operator received no clear startup-level
signal — exactly the silent-degradation failure mode the fail-fast was
designed to prevent.
Replace the blanket catch with specific catches for the two
runtime-recoverable exception types the secondary tool caller can throw
after the transport/SDK/cancellation handlers:
- JsonException — upstream returned malformed JSON
- KeyNotFoundException — upstream JSON missing required fields
(defensive; TryGetProperty guards most paths after wave-1)
Both map to the existing "upstream_malformed" structured-error taxonomy.
All other exceptions, including configuration errors like
ApiKeyResolver's InvalidOperationException, now propagate so a
misconfigured deployment fails loudly at the operator layer instead of
limping along behind a generic user-facing message.
ASP.NET Core hands control back to the middleware once the endpoint handler
completes via `await _next(context)`, but for streaming endpoints (AG-UI uses
`IAsyncEnumerable`/SSE) the response delegate continues writing to the response
body — and may issue downstream OpenAI calls — AFTER `_next` returns. The
previous `finally` clause reset `AimockHeaderContext` to an empty dictionary at
that point, which caused `AimockHeaderPolicy` to silently drop aimock headers
on those streaming-tail calls. That defeated the entire purpose of the
middleware for the exact codepath it was built to serve.
The `finally`-clear was also unnecessary for isolation: `AsyncLocal<T>` is
scoped to the current execution context, so each ASP.NET request gets its own
slot automatically. There is no cross-request leakage to defend against.
Remove the `try`/`finally` and just call `AimockHeaderContext.Set(headers)`
followed by `await _next(context)`. Add a comment documenting why no reset is
performed.
ApiKeyResolver.IsMockEndpoint compared Uri.Host against "[::1]", but
System.Uri.Host strips the surrounding brackets and returns "::1" for
inputs like http://[::1]:8000/. The bracketed comparison was therefore
dead code: developers running aimock on IPv6 loopback got the fail-fast
error instead of the intended mock-key fallback.
Change the literal to "::1" and update the inline + docstring comments
to reflect the bracket-less form.
- BeautifulChatStateSnapshotAgent.RunCoreAsync now coalesces a null
response.Messages to an empty array before constructing the new
List<ChatMessage>. Previously, an inner agent that returned no
messages would cause `new List<ChatMessage>(null)` to throw
ArgumentNullException, masking the real upstream behaviour.
- GenerateA2ui's empty-content branch now returns the canonical
BeautifulChatA2ui.StructuredError shape (error / message /
remediation / errorId) like every other error path in the method,
instead of an ad-hoc { error, errorId } object. The frontend
expects the structured shape with remediation guidance.
Program.cs#CreateOpenAiClient previously read the OpenAI endpoint solely from
the OPENAI_BASE_URL environment variable and fell back to
ApiKeyResolver.DefaultOpenAiEndpoint. The secondary tool-calling HTTP client
(A2uiSecondaryToolCaller) instead routes through ApiKeyResolver.ResolveEndpoint,
which checks env, then configuration[OPENAI_BASE_URL] (appsettings.json /
user-secrets), then the default.
When OPENAI_BASE_URL was supplied only via configuration (e.g. user-secrets in
local dev), the primary client silently dialed the public Azure-hosted models
endpoint while the secondary client dialed the configured aimock. The two
endpoints diverged with no visible signal, breaking aimock-routed flows and
masking misconfigurations.
Switch CreateOpenAiClient to call ApiKeyResolver.ResolveEndpoint so both
clients share a single source of truth. Logging is preserved and now reports
which source supplied the endpoint (env, configuration, or default).
When iterating IHeaderDictionary's underlying store yields case-variant
duplicates (e.g., a misbehaving proxy injecting both `X-Foo` and `x-foo`),
the previous GroupBy + ToDictionary path silently kept only the first value
and discarded the rest. For aimock context routing — where header semantics
drive fixture selection — silent drops are a debugging nightmare.
We still keep `.First()` because HTTP defines no canonical merge for
case-variant collisions across distinct keys (the comma-join rule only
applies when keys are ASCII-equal). Instead, when a group has more than
one entry, we emit a structured warning naming the key, the kept value,
and the number of dropped variants, so operators can spot the upstream
misbehavior in logs.
Constructor now takes `ILogger<AimockHeaderMiddleware>` — ASP.NET's
middleware activator injects it automatically via `UseMiddleware<T>()`.
Two surgical hardening fixes to ApiKeyResolver:
1. IsMockEndpoint: drop host.StartsWith("aimock.", ...) and
host.EndsWith(".aimock", ...). The StartsWith match is exploitable
— an attacker-registered domain like aimock.attacker.example.com
would be classified as a mock endpoint and bypass the fail-fast,
silently returning the mock key. The EndsWith match is unused
noise (no .aimock TLD exists). Only exact, well-known mock hosts
("localhost", "127.0.0.1", "0.0.0.0", "[::1]", "aimock") are
accepted. IPv6 loopback [::1] is added so dual-stack dev paths
are still covered.
2. ResolveApiKey: change !string.IsNullOrEmpty(apiKey) to
!string.IsNullOrWhiteSpace(apiKey). A whitespace-only key such as
" " would otherwise be accepted as valid, bypassing the mock-key
/fail-fast logic entirely and sending a bogus key upstream.
- RunCoreAsync: avoid mutating AgentResponse.Messages in place. Reassign
Messages to a fresh List<ChatMessage> so the path is safe regardless of
whether the inner agent returns a mutable list or an immutable
IReadOnlyList wrapper. Prevents NotSupportedException at runtime; all
other response metadata (AgentId, ResponseId, Usage, RawRepresentation,
...) is preserved via the property setter.
- RunCoreStreamingAsync: only emit the trailing todos snapshot if the
inner stream completed normally. Wrap the inner enumerator in
try/finally with a streamCompletedNormally flag; on early exit
(throw or cancellation) log a warning and skip the snapshot rather
than emitting potentially stale state to the frontend. The trailing
yield lives outside the try block (idiomatic C# pattern since `yield`
cannot live inside `try { } catch { }`).
The previous implementation used Contains() substring matching against the
full endpoint URL, which is exploitable. An attacker-controlled endpoint
such as https://attacker.example.com/aimock-decoy or
https://api.openai.com/?env=localhost would be classified as a mock and
bypass the fail-fast guard, silently returning the sk-mock-local key for
what is actually a non-mock destination.
Parse the URL and inspect only the host component, matching exact dev
hosts (localhost, 127.0.0.1, 0.0.0.0, aimock) plus aimock subdomains.
Path, query, and arbitrary subdomain segments containing "aimock" or
"localhost" no longer trigger the mock fallback.
The header propagation chain (middleware -> context -> policy) had two bugs
that combined to threaten D5/D6 header-forwarding:
1. AimockHeaderMiddleware.ToDictionary used the default ORDINAL case-sensitive
comparer. ASP.NET's IHeaderDictionary is case-insensitive, but iterating
the underlying store can yield case-variant duplicates (e.g., a misbehaving
proxy injecting both `X-Foo` and `x-foo`). Default-comparer ToDictionary
throws ArgumentException on duplicates and fails the request.
2. AimockHeaderContext.Set lowercased all keys via ToLowerInvariant.
AimockHeaderMiddleware captured original case; the context then mutated
the casing; AimockHeaderPolicy.TryGetValue then matched against whatever
case the OpenAI SDK happened to use. This is inconsistent and aimock
fixture matching can be case-sensitive depending on configuration.
Canonical strategy: preserve original header casing as captured by the
middleware, but compare case-insensitively throughout via
StringComparer.OrdinalIgnoreCase. The middleware now also groups variants
defensively so duplicate keys cannot blow up the dictionary build.
AimockHeaderPolicy's add-if-absent TryGetValue then works correctly under
case-insensitive comparison without any further changes.
The GitHubToken line shipped `ghp_...` as a literal placeholder VALUE. When
a developer runs `cp .env.example .env` and forgets to edit, the entrypoint's
`-z "$GitHubToken"` check sees a non-empty string and skips the "key missing"
warning. The literal `ghp_...` is then sent upstream, producing a confusing
401 that looks like an OpenAI/GitHub Models bug rather than a setup issue.
Move the format example into a comment above the line and leave the value
empty so the empty-value check in entrypoint.sh fires the clear warning.
OPENAI_API_KEY was already empty (prior fix); verified still empty.
- entrypoint.sh: replace `sleep 3 && kill -0` agent-startup gate with a curl
retry loop against /health on :8000 (up to 30s). The bare PID check only
proved the process existed; if Kestrel hadn't finished binding, Next.js
would proxy to a dead backend for ~90s until the watchdog killed the
container.
- entrypoint.sh: watchdog now also supervises Next.js. If Next.js dies while
the agent stays healthy the watchdog breaks out so wait -n can return and
Railway can restart the container instead of serving a broken page.
- entrypoint.sh + .env.example: align the env-var contract with what
agent/Program.cs actually reads. The .NET agent uses OPENAI_API_KEY,
GitHubToken (fallback), and optional OPENAI_BASE_URL — it never reads
AZURE_OPENAI_API_KEY. The startup warning and .env.example now document
the real key precedence plus every Next.js-side var (AGENT_URL,
NEXT_PUBLIC_BASE_URL, MCP_SERVER_URL, SHOWCASE_DEBUG_TOKEN).
- Replace silent `return null` paths in A2uiSecondaryToolCaller with
TryGetProperty guards that each emit a structured LogWarning naming
the exact missing/unexpected field (choices, message, tool_calls,
function, name mismatch, arguments). Callers can now tell why a
design-tool call produced no content.
- Add ILogger parameter to GetDesignToolArgumentsAsync and pass
BeautifulChatAgent._logger from the single call site so warnings
flow into the existing log stream.
- Log the response body (truncated to 1 KB) at LogWarning before
EnsureSuccessStatusCode throws, so upstream error payloads survive
the throw and reach operators.
- Extract the OPENAI_API_KEY/GitHubToken/sk-mock-local fallback chain
from Program.cs and A2uiSecondaryToolCaller.cs into a new
ApiKeyResolver helper. Both call sites now share one implementation.
- ApiKeyResolver fails fast with InvalidOperationException + LogCritical
when no real key is present and OPENAI_BASE_URL is not an
aimock/localhost endpoint, so misconfigured prod deploys cannot
silently send sk-mock-local to a real LLM provider. The silent
mock-key fallback is preserved for aimock/localhost dev endpoints.
- RunCoreAsync: append todos snapshot DataContent to AgentResponse so non-streaming
callers receive the same state mirror that RunCoreStreamingAsync already emits.
- ManageTodos: defensive-copy each incoming todo before storing (mirrors the
symmetry of GetTodosSnapshot) so callers cannot mutate our backing list by
retaining input references.
- ManageTodos: validate Status against the documented "pending" | "completed" set;
coerce out-of-range values to "pending" with a LogWarning instead of letting
arbitrary LLM-supplied strings into shared state.
- GenerateA2ui: add a final catch (Exception) returning a StructuredError
("unexpected_error", ...) matching the existing taxonomy, and log an info-level
entry when OperationCanceledException flows through (was previously silent).
AimockHeaderPolicy previously called message.Request.Headers.Set(...)
unconditionally for every key returned by AimockHeaderContext, which
silently clobbered any header already set by an earlier pipeline policy
or the SDK itself (e.g. x-request-id, x-correlation-id).
Switch both Process and ProcessAsync to add-if-absent semantics, using
PipelineRequestHeaders.TryGetValue to skip keys that already have a value
on the outbound request. New aimock x-* headers still propagate; existing
correlation/SDK headers are preserved.
Aligned everything to npm to match what the Dockerfile already uses
(`npm ci --legacy-peer-deps`) and the committed `package-lock.json`.
The sibling `ms-agent-dotnet` integration uses the same npm-based
setup, so npm is the established convention.
Changes:
- playwright.config.ts: webServer.command now `npm run dev` (was `pnpm dev`),
so local E2E works in environments without pnpm installed.
- package.json: removed the redundant top-level `pnpm.overrides` block.
The equivalent override is already declared under npm's `overrides`
field, so the pnpm block was dead weight given that npm is canonical.
- package.json: `scripts.dev` now uses `concurrently -k --success first`
so a crashing .NET agent surfaces during local dev instead of leaving
Next running headlessly.
Each integration's playwright.config.ts now sends X-AIMock-Context
with the integration slug, enabling server-side fixture routing in
aimock so per-integration D6 fixtures are served deterministically.
R3b bumped the monorepo packages but missed two showcase-level override
pins. The langgraph-typescript integration pins @ag-ui/langgraph directly
in both its top-level and src/agent package.json files, bypassing whatever
@copilotkit/runtime transitively resolves.
Without this bump, the showcase LGT Docker image bakes 0.0.32 even though
the monorepo runtime/sdk-js are on 0.0.33 (R3b). This is what's keeping
the D6 LGT probe RED.
Path filter showcase/** matches, so showcase_build.yml fires on merge to
rebuild + Railway-redeploy langgraph-typescript with the real 0.0.33.
Picks up _extract_forwarded_headers_from_config from PR #4984, now
shipped as copilotkit 0.1.91 on PyPI. Three Python integrations move
forward together: langgraph-python, strands, langgraph-fastapi.
Updates validate-pins ratchet hash (count stays 106, FAIL set shifted
because showcase pins now diverge from Dojo on 0.1.91 vs 0.1.87).
Showcase auto-redeploys to Railway on merge via showcase_build.yml
(path filter showcase/**).
## Summary
Wires per-request x-* headers through the CopilotKit Python middleware
so LangGraph-based agents receive the original request's forwarded
headers (D6 "everything works" prerequisite). Four logical pieces:
1. **sdk-python forwarded-header extraction** —
`_extract_forwarded_headers_from_config()` reads x-* headers from
LangGraph's runtime config (both wrapper-dict
`copilotkit_forwarded_headers` and raw x-* keys), applies documented
precedence (context > configurable, wrapper > raw), lowercases keys at
insertion to make precedence deterministic across mixed-case headers,
and always clears the ContextVar on early-exit paths so stale headers
from a prior request cannot leak.
2. **sdk-python tests** — 47 new/modified test cases covering
wrapper-dict and raw extraction, context > configurable precedence,
mixed-case normalization, RuntimeError early-return clearing,
exception-path clearing, None/empty-config fallbacks, sync/async parity.
3. **Showcase Python pins** — pins `copilotkit==0.1.90` across
langgraph-python, strands, and langgraph-fastapi so the version that
runs in showcase matches the version that contains this fix. Bumps
`ag-ui-langgraph[fastapi]>=0.0.35` in langgraph-fastapi because
copilotkit 0.1.90 requires it transitively (the previous `==0.0.34` pin
would cause `pip install` to hard-fail).
4. **Showcase docker-compose** — adds
`LANGGRAPH_HTTP={"configurable_headers":{"include":["x-*"]}}` to the
shared `x-integration-defaults` anchor so langgraph-api includes x-*
headers in the runtime config; without this, langgraph-api 0.7+ strips
x-* headers before the agent ever sees them.
## Companion PR
Depends on the matching ag-ui PR that adds per-request header forwarding
across 5 integration adapters (langgraph, mastra, vercel-ai-sdk,
langchain, claude-agent-sdk). After ag-ui releases, bump the
`@ag-ui/langgraph` pin in `packages/sdk-js/package.json` in a follow-up.
## CR loop
- Round 1: surfaced 12 findings (4 ag-ui clusters + 6 CopilotKit
clusters) across 14 reviewers.
- Round 2 fix: docker-compose LANGGRAPH_HTTP YAML merge bug, sdk-python
wrapper-dict precedence, exception-path ContextVar leak,
RuntimeError-path leak, langgraph-fastapi version conflict.
- Round 2 confirmation: 14 reviewers, surfaced 4 new Bucket (a) findings
on CopilotKit side.
- Round 3 fix: lowercase-at-insertion + always-clear ContextVar on both
early-exit paths + ag-ui-langgraph[fastapi] bump.
- Round 3 confirmation: 7 reviewers, all NO_BUCKET_A_FINDINGS.
- Pre-push-quality: green (ruff, 47+87 pytests, build, docker-compose
config).
## Test plan
- [ ] CI green
- [ ] Showcase D6 LangGraph integration receives x-aimock-context header
end-to-end with x-AIMock-Strict propagation
- [ ] No regressions in other Python integrations (strands,
langgraph-fastapi)
- [ ] Docker compose still boots all integrations with the new shared
LANGGRAPH_HTTP env
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Five post-cutover follow-ups bundled together because all surfaced in
the same spot-check pass on `/integration/<page>` routes.
## 1. Tag `page-send-message` region (`4680eb9c1`)
`/langgraph-python/programmatic-control` and
`/google-adk/programmatic-control` rendered a yellow "Missing snippet"
callout because `<Snippet region="page-send-message" />` had no matching
`// @region[page-send-message]` / `// @endregion[page-send-message]`
pair in the resolved `headless-complete` cell. Peer integrations
(mastra, ag2, strands, pydantic-ai, llamaindex, langgraph-fastapi,
crewai-crews, …) already had the tags; only north-star and its ADK
mirror were missing them. The region wraps the connect / send / stop
block in `chat/chat.tsx`.
## 2. Suppress HubSpot-rewritten href hydration mismatch on nav-bar
(`2c0791930`)
HubSpot's analytics tag (loaded from `js-na2.hs-analytics.net`) rewrites
the Intelligence CTA's outbound `href` client-side to append `__hstc` /
`__hssc` / `__hsfp` cross-domain tracking params. Server-rendered HTML
keeps the bare URL, post-hydration DOM has the rewritten URL, React's
hydration diff fires.
Add `suppressHydrationWarning` to the two anchor elements that point at
`INTELLIGENCE_CTA_HREF` (desktop BrandNav `LEFT_LINKS` entry,
MobileTopNav Lightbulb icon).
## 3. Register `UseAgentSnippet` (`f809b9b8b`, expanded by `773631cbd`)
`inlineSnippets()` in `docs-render.tsx` maintains its own `SNIPPET_MAP`
separate from `mdx-registry.tsx`'s `STUB_PARTIAL_MAP`. The two
registries drifted. `UseAgentSnippet` was the most-hit miss, but Railway
logs surfaced 14 more: `InstallSDKSnippet`, `InstallPythonSDK`,
`RunAndConnect` (+ `Snippet` alias), `CopilotUI`, `LandingCodeShowcase`,
the four `CopilotCloudConfigure*` / `SelfHostingCopilotRuntime*` keys,
plus `MigrateTo` / `MigrateToV` / `ToolRenderer` aliases. All added.
## 4. Make `inlineSnippets()` code-fence-aware + add Icon-suffix
heuristic (`773631cbd`)
After the registry fix, the remaining `[docs-render] snippet missing`
log entries split into two false-positive classes:
- **Code-fence false positives.** The regex matched `<Component />`
references inside ` ```tsx ``` ` example blocks — e.g. `<CopilotChat />`
/ `<CopilotSidebar />` shown as runtime usage, `<WeatherCard />` /
`<YourApp />` as placeholders. A new `isInsideCodeFence(content,
offset)` helper tracks fenced blocks (matching any indentation — MDX
inside `<Step>` is routinely 8-space-indented) and inline-code spans.
Replaces the ad-hoc `CopilotChat`-only allowlist from commit 3.
- **JSX-prop runtime components.** `icon={<PaintbrushIcon />}` etc. are
real React components from `mdx-registry.tsx::docsComponents`, not
snippets. Add an `Icon`-suffix heuristic: lucide icons used as JSX props
are silenced.
## 5. Suppress HubSpot hydration mismatch on `<OpsPlatformCTA>` +
`<SignupLink>` (`10b4960a3`)
Same HubSpot rewrite hits every dashboard.operations.copilotkit.ai
outbound link. Add `suppressHydrationWarning` to all four `<a>` tags in
`OpsPlatformCTA` (`info` / `inline` / `tile` / `card` variants) and the
single `<a>` in `SignupLink`. Observed live as a hydration error on
`/<framework>/prebuilt-components`, `/<framework>/headless`, and any
page that embeds an Intelligence-platform CTA.
## Verification
- `grep -n "@region\[page-send-message\]"
showcase/integrations/{langgraph-python,google-adk}/src/app/demos/headless-complete/chat/chat.tsx`:
both files have start (line 38) + end (line 114) markers; `diff` between
them is empty post-change.
- `npx tsx showcase/scripts/bundle-demo-content.ts`: regenerated
`demo-content.json` exposes `regions["page-send-message"]` for both
`langgraph-python::headless-complete` and
`google-adk::headless-complete` (1878 bytes, `chat/chat.tsx` lines
38-112).
- Playwright sweep across `/programmatic-control`,
`/runtime-server-adapter`, `/frontend-tools`,
`/generative-ui/tool-rendering`, `/prebuilt-components`,
`/deploy/agentcore`, `/auth` on `google-adk` and `mastra`: 0 console
errors, 0 warnings, 0 "Missing snippet" callouts in rendered DOM, both
desktop (1440px) and mobile (390px) viewports.
## Test plan
- [ ] Pull, build shell-docs, smoke
`/langgraph-python/programmatic-control` and
`/google-adk/programmatic-control`: yellow "Missing snippet" callout is
gone.
- [ ] Same pages on a mobile viewport: no hydration warning in the
console.
- [ ] `/<framework>/prebuilt-components` and any page with an inline
`<OpsPlatformCTA>`: no hydration warning.
- [ ] Peer integration pages (e.g. `/mastra/programmatic-control`,
`/<framework>/deploy/agentcore`, `/<framework>/frontend-tools`):
snippets still render, no `[docs-render] snippet missing` warnings.
- [ ] Redeploy shell-docs.
## Out of scope
- Underlying prose-vs-code parity gap on the headless-complete cell
(north-star uses `agent.abortRun()` and skips `connectAgent`) is tracked
separately.
- Unifying `docs-render.tsx::SNIPPET_MAP` and
`mdx-registry.tsx::STUB_PARTIAL_MAP` into a single source of truth (so
future entries can't drift) is the right architectural follow-up. Filed
separately.
- Environmental jsdom × vitest interaction blocking
`packages/web-inspector/src/lib/__tests__/telemetry.test.ts` (which
forced `--no-verify` on these commits) is tracked separately.
The programmatic-control docs page renders a yellow "Missing snippet"
box on the langgraph-python and google-adk variants because their
headless-complete cells were never tagged with the page-send-message
region the MDX requests. Add matching @region / @endregion markers
around the useAgent / useCopilotKit / send / reset block in
chat/chat.tsx so the Snippet component resolves on both integrations.
Pin copilotkit==0.1.90 across the three CopilotKit-aware Python
integrations (langgraph-python, strands, langgraph-fastapi) so the
forwarded-header extraction from this PR is the version that runs in
showcase. Bump ag-ui-langgraph to >=0.0.35 with the [fastapi] extra in
langgraph-fastapi because copilotkit 0.1.90 requires it transitively;
the previous ==0.0.34 pin would cause pip install to hard-fail.
The "next" dist-tag was a workaround for Docker builds that can't resolve
workspace:* — but "next" has gone stale (1.55.2-next.1) while "latest" is
at 1.56.5. Renovate doesn't cover showcase/, so these never auto-bumped.
Switch all 19 showcase package.json files to "latest".