## What & why
Resolves [OSS-132](https://linear.app/copilotkit/issue/OSS-132).
Investigated with systematic-debugging; every conclusion verified
against the **real** OpenAI Responses API.
**Net change: a TanStack version bump only.** No showcase schema change.
- `@tanstack/ai` `0.18.0` → `0.35.0`
- `@tanstack/ai-openai` `0.9.1` → `0.15.6`
- `package-lock.json` regenerated (Dockerfile uses `npm ci
--legacy-peer-deps`)
## The bug
The built-in-agent showcase 400s on every prompt against real OpenAI.
The state tools (`AGUISendStateSnapshot` / `AGUISendStateDelta` /
`set_steps`) declare arbitrary payloads as `z.any()`, which serializes
to a **typeless** JSON-Schema property (`{ "description": ... }`, no
`"type"`).
The old `@tanstack/openai-base`'s `isStrictModeCompatible()` only
screened for `oneOf/allOf/not/$ref/$defs`, so it missed the missing
`type`, sent the tool with `strict: true`, and OpenAI rejected it:
```
400 Invalid schema for function 'AGUISendStateSnapshot':
In context=('properties','snapshot'), schema must have a 'type' key.
```
This was **masked in production** because the deployed showcase runs
against aimock, which replays fixtures without validating the request
schema — a raw `curl` to prod returns a clean `RUN_FINISHED`, green for
the wrong reason.
The ticket's original framing (zod3/zod4 drift → typeless *root*, `got
"None"`) was already fixed by the zod-4 migration; this is the same
symptom one layer down (typeless *property*).
## The fix is upstream
`@tanstack/ai-openai@0.15.6` (via `@tanstack/openai-base@0.9.2`) fixes
`isStrictModeCompatible`: it now detects typeless / `z.any()` properties
and sends `strict: false`. OpenAI accepts typeless properties under
`strict: false` — so `z.any()` works again with no schema change on our
side.
(`@tanstack/ai-openai@0.15.5` also dropped `@tanstack/ai-client` from
its peerDependencies, so no `ai-client` dep is added.)
## Verification (real OpenAI, gpt-4o)
| Probe | Result |
|---|---|
| Typeless property, `strict: true` (raw OpenAI) | **400** — `schema
must have a 'type' key` |
| Typeless property, `strict: false` (raw OpenAI) | **ACCEPTED** —
confirms it was the strict flag, not the schema |
| `z.any()` tool on old adapter (0.9.1/0.15.4) | adapter sends `strict:
true` → **400** |
| `z.any()` tool on new adapter (0.15.6) | adapter sends **`strict:
false`** → **ACCEPTED**, model calls the tool |
| All 3 `z.any()` state tools attached, new adapter | **ACCEPTED**, no
400 |
## Not covered here
The showcase's aimock + Playwright e2e suite was **not** run locally
(this worktree has no installed toolchain). CI runs it on this PR;
please confirm the gen-ui / shared-state demos still pass before merge.
---
_Branch history shows an interim `z.string()` workaround that was
reverted once the upstream fix shipped; the net diff is the version bump
only. Squash-merge recommended._
built-in-agent was the lone integration left on the legacy auth variant
when 5057efce1a brought the other 19 into conformance ("built-in-agent
already passes via its ChatErrorBoundary"). It rendered the post-sign-out
401 via a React ChatErrorBoundary (auth-demo-chat-boundary) + a local
401-regex onError, and defaulted to authenticated on first paint — so the
byte-identical auth.spec.ts (the CI conformance gate) failed every
unauth-first assertion.
Normalize to the langgraph-python gold shape:
- use-demo-auth.ts: unauth-first hook (hasEverSignedIn/signIn/signOut,
localStorage-backed token, isAuthenticated/authorizationHeader).
- page.tsx: drop ChatErrorBoundary/lastError/local-401-regex; wire a shared
handleAuthError onto BOTH <CopilotKitProvider onError> and the agent-scoped
<CopilotChat onError>; clear-on-auth useEffect keyed off authError alone;
unauth-first SignInCard gate; amber [data-testid="auth-demo-error"] surface.
- auth-banner.tsx / sign-in-card.tsx: align prop contract to gold
(onSignIn, onSignIn(token)).
Forced divergences preserved: built-in-agent IS the built-in agent, so it
keeps <CopilotKitProvider> (runtime registers the agent under the default
key) rather than <CopilotKit agent="auth-demo">, and uses raw Tailwind
elements (no shadcn @/components/ui in this integration). The error-handling
shape, auth hook, and testid contract match gold exactly.
Proven RED->GREEN on the byte-identical auth.spec.ts (the discriminating
surface; the --d5 probe accepts both shapes and was green for the legacy
frontend): all unauth-first conformance assertions flip FAIL->PASS, and the
canonical built-in-agent:auth --d5 --isolate probe is green.
Bump the canonical CopilotKit pin across all showcase integrations + shell
to 1.61.2 (canonical-pins.json, every package.json + package-lock.json),
which carries CopilotKit#5611: passing a catalog to the provider
(`<CopilotKit a2ui={{ catalog }}>`) now auto-enables A2UI and defaults tool
injection on, so the runtime no longer needs an explicit `a2ui` config.
Demonstrate the feature on the A2UI dynamic (declarative-gen-ui) demos by
removing the now-redundant runtime `a2ui` block (`injectA2UITool: true` +
`defaultCatalogId`) from:
- langgraph-python, langgraph-fastapi, langgraph-typescript
- strands, strands-typescript
- google-adk
The forwarded catalog supplies its own catalogId (sdk-js A2UI middleware
auto-derives `defaultCatalogId` from it), so the previous "Catalog not found"
fallback no longer applies.
Verified: validate-pins drift ratchet unchanged (38 / same hash);
langgraph-python D6 `gen-ui-declarative` green end-to-end (no Catalog-not-found).
The secondary-LLM prompt was far thinner than the canonical generation guidelines,
so it emitted trees that (correctly) failed the renderer's paint gate → surface-missing.
Port the canonical generation rules into the prompt, add output validation, add catalog
parity (DataTable + info-row), ground the planner with sales-context, and record
multi-turn aimock fixtures. Includes CR fixes: two-arg z.record for the DataTable rows
schema (zod@4 API), index-based DataTable row key, and Metric trendValue rendering for
neutral trend.
The showcase authors A2UI catalog defs with root zod@4, but @a2ui/web_core's
GenericBinder schema scraper inspects Zod-3 internals (_def.typeName==='ZodUnion').
A zod@4 union reports _def.typeName===undefined → misclassified STATIC → the raw
{path} binding object reaches render → React error #31. Author this demo's catalog
with a zod-v3 (npm:zod@3.25.76) alias so the binder resolves bindings. Includes CR
hardening of the shared a2ui factory validation (plain-object data guard, unique-id
check, fail-loud on non-string secondary-LLM return).
The built-in-agent gen-ui-agent D6 cell already passes end-to-end locally;
the PARITY_NOTES entry that documented it as RED/blocked on a STATE_DELTA
to useAgent gap in @copilotkit/react-core was stale. The set_steps to
STATE_DELTA {op:"add", path:"/steps"} workaround merged in
tanstack-factory.ts closed that gap: @ag-ui/client applies the patch and
fires onStateChanged, the core state-manager fans it to subscribers, and
useAgent re-renders off agent.state.steps. No react-core change is needed.
Rewrites the gen-ui-agent entry to GREEN/reclaimed and rescopes the
section header to the remaining A2UI render-layer demos (a2ui-fixed-schema,
declarative-gen-ui), whose fixes belong to @copilotkit/a2ui-renderer, not
react-core. Doc-only; no config quarantine existed (gen-ui-agent was never
in manifest not_supported_features), so the cell stays a counted green.
Local RED baseline: cell passes (1 passed) despite the stale RED doc.
Local GREEN value-test: --repeat 3 => 3 passed (130.1s), stable.
@tanstack/ai-openai@0.15.6 (via @tanstack/openai-base@0.9.2) fixes the upstream
isStrictModeCompatible bug: it now detects typeless / z.any() properties and
sends strict:false, so OpenAI accepts them instead of 400ing with
"schema must have a type key".
With the upstream fix the showcase needs no schema change: revert the
z.string() workaround so the state tools use z.any() again (state-tools.ts is
now identical to main). Net change vs main is just the version bump:
@tanstack/ai 0.18.0 -> 0.35.0, @tanstack/ai-openai 0.9.1 -> 0.15.6.
Verified on the real OpenAI Responses API with all three z.any() state tools
attached (AGUISendStateSnapshot / AGUISendStateDelta / set_steps): adapter
emits strict:false, request ACCEPTED, model calls the tool.
@tanstack/ai-openai@0.15.5 dropped @tanstack/ai-client from its
peerDependencies (now just zod ^4 and @tanstack/ai ^0.35.0), and the
showcase never imported ai-client anyway. Bump to the latest releases and
remove the ai-client dep entirely.
Tool-schema serialization is unchanged: the z.string() state-tool params
still emit a strict-valid schema (root type:object, snapshot type:string,
strict:true), verified by capturing the outbound /responses tool payload on
0.35.0 / 0.15.5.
The built-in-agent showcase 400s on every prompt against the real OpenAI
Responses API. The state tools (AGUISendStateSnapshot / AGUISendStateDelta /
set_steps) declared their arbitrary payloads as z.any(), which serializes to
a typeless JSON Schema property. @tanstack/openai-base's
isStrictModeCompatible only screens for oneOf/allOf/not/$ref/$defs, so it
misses the missing type, sends the tool with strict:true, and OpenAI
rejects it:
400 Invalid schema for function 'AGUISendStateSnapshot':
In context=('properties','snapshot'), schema must have a 'type' key.
This is the real-OpenAI form of OSS-132 (the earlier zod3/zod4 drift that
produced a typeless root is already fixed by the zod 4 migration). It was
masked in production because the deployed showcase runs against aimock,
which replays fixtures without validating the request schema.
Fix: declare each arbitrary payload as a JSON-encoded string (z.string() ->
{ type: string }, strict-valid) and parse it in the server handler.
parseJson also tolerates an already-parsed object/array so recorded aimock
fixtures keep working. The TanStack->AG-UI converter reads the parsed
structure off the tool result, so it needs no change. Verified accepted and
correctly round-tripped by gpt-4o on the real Responses API with all three
tools attached.
Also bump @tanstack/ai 0.18.0 -> 0.34.0 and @tanstack/ai-openai 0.9.1 ->
0.15.4, and add the newly-required @tanstack/ai-client 0.18.2 peer. Adapter
schema output is byte-identical across the bump.
Bump every @copilotkit/* dependency across the showcase integrations and
the shell from 1.60.2 (and stray "latest" override pins) to an exact
1.61.1 pin, and move the canonical pin source of truth to match.
Regenerate each standalone npm package-lock.json with the same
--legacy-peer-deps flag the Dockerfiles use for "npm ci".
- showcase/integrations/*/package.json + package-lock.json
- showcase/integrations/langgraph-typescript/src/agent/*
- showcase/shell/package.json + package-lock.json
- showcase/scripts/showcase-canonical-pins.json: canonical 1.60.2 to 1.61.1
aimock stays on its own version line (1.26.1). The Python copilotkit SDK
was already 0.1.94 across every requirements.txt, so no change there.
validate-pins ratchet is unchanged (FAIL=38, identical hash);
validate-parity, validate-fixture-tool-surface, and the showcase/scripts
vitest suite (2102 tests) all pass.
The built-in-agent shared-state-read-write demo wrote agent state via
direct property assignment (`agent.state = {...}`), which sets the value
but does not fire `onStateChanged` — so subscribed components never
re-render off a UI write. Every other framework demo (including the
langgraph-python gold standard) already uses `agent.setState(...)`, which
both updates state and notifies subscribers. This was the lone divergence.
Switch the seed and the title write to `agent.setState(...)` so the demo
matches the documented pattern (docs/shared-state.mdx says writes go
through `setState`) and the rest of the showcase.
Verified: built-in-agent D5 e2e-deep suite green after rebuild.
Adds a durable persistence layer for @copilotkit/bot, replacing the
in-memory-only ActionStore with a pluggable StateStore.
- StateStore interface (kv/list/lock/dedup/queue) with a shared
conformance suite; MemoryStore default plus @copilotkit/bot-store-redis
and @copilotkit/bot-store-postgres backends.
- createBot({ store }): typed per-thread state via Standard Schema,
action snapshots persisted through the store, per-conversation turn
lock (onLockConflict drop|force), and inbound-event dedup keyed on a
stable eventId. ActionStore is kept as a deprecated alias.
- Cross-platform transcripts (bot.transcripts + identity resolver) with
age-bounded retention (prune on append + filter on read), and
runAgent({ transcript: true }) to auto-inject history and capture the
reply.
- createBot({ components }) re-registers components so durable actions
re-fire after a restart; restart-durability demo in examples/slack.
- Dedup is marked seen only after the turn lock is acquired, so a turn
dropped on lock-conflict does not burn its eventId (no lost retries).
- Release lockstep: bot-store-redis/postgres version with bot + bot-ui.
schema.json regenerated from the merged canonical schema.ts (failure_classifier
probe.exit additions UNION backend request.ingress/sse.first_byte/llm.call.*
boundaries + test_id adoption). Per-integration staged schema.ts copies
re-derived via 'showcase cvdiag-stage-ts' so codegen --check and stage --check
are both in sync. No hand-merge of generated artifacts.
Aligns dependency versions across all 19 showcase integrations to current
released minor versions for the 1.60.2 release cycle.
Package families:
- @copilotkit/{a2ui-renderer, react-core, react-ui, runtime, shared, sdk-js, voice}
1.59.4 -> 1.60.2 (18 integrations already staged; ms-agent-harness-dotnet
catches up from 1.57.2)
- @ag-ui/{client, core, encoder} 0.0.55 -> 0.0.57
- @ag-ui/mastra 0.2.1-beta.2 -> 0.2.4 (stable on 0.x; 1.0.x major held back)
Includes the previously-missed ms-agent-harness-dotnet integration in the
@copilotkit/* bump, plus the @copilotkit/web-inspector override pin.
Lockfile-only reconciliation via npm install --package-lock-only
--legacy-peer-deps (cmdk@0.2.1 pre-existing react^18 peer-dep is unaffected).
* duplicate ceiling 290→291: tool-rendering.json's tightened 'current
price of AAPL' matchers now share two match keys with the existing
tool-rendering-custom-catchall.json entries in the same BIA context,
runtime-disambiguated by feature route.
* shadow ceiling 134→132 (ratchet down): the bare 'AAPL' vs 'current
price of AAPL' shadow pair on the tool-rendering.json side is gone.
* PARITY_NOTES: replaces the 'headless-complete turns 3+4 server-tool
reprompt loop' known-issue section with a resolved-via-sequenceIndex
description; the architectural reprompt loop now converges via the
sequenceIndex-gated emitter + narration-fallback pattern in
gen-ui-headless-complete.json.
The injected render_a2ui tool guide instructs models to omit catalogId
("the catalog id is set by the host"), and backend-owned generate_a2ui
tools see real models omit or late-stream it. Without defaultCatalogId
the a2ui middleware falls back to the spec basic catalog, which no
showcase page registers — surfaces fail with "Catalog not found:
https://a2ui.org/specification/v0_9/basic_catalog.json" (reported on
beautiful-chat / langgraph-python).
Pin each route to the catalog its page registers: beautiful-chat ->
copilotkit://app-dashboard-catalog, declarative-gen-ui ->
declarative-gen-ui-catalog. Routes with no a2ui block never attach the
middleware and are left untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Documentation companion to the BIA D6 readiness work:
- PARITY_NOTES.md: tracks built-in-agent feature parity vs LGP +
a 'Known Issues' section documenting 3 D6 demos that remain red
due to downstream renderer/state gaps in @copilotkit/react-core
(a2ui renderer host doesn't mount Card; STATE_DELTA → useAgent
state-subscription gap; declarative-gen-ui renderer host shares
the same class). All three require fixes outside this PR's scope.
- feature-registry.json: adds threadid-frontend-tool-roundtrip
feature entry covering the new gen-ui-agent STATE_DELTA wiring.
Adds the set_steps tool definition to state-tools and wires it through
the tanstack factory so the gen-ui-agent demo emits STATE_DELTA events
on its agent-plan stream the same way the LGP integration does.
Extracts the inline subagents page DelegationCard into three reusable
components that mirror the LGP integration layout, enabling D6 testid
coverage:
- subagent-activity-card.tsx: per-subagent activity panel
- delegation-log.tsx: cumulative delegation event log
- supervisor-activity-banner.tsx: supervisor-level status banner
The subagents page now composes these components instead of inlining
the markup, with data-testid hooks for D6 selectors.
Annotates 14 integration manifests with d6_not_supported_features entries
and aligns d6_supported_features with what each backend actually implements,
so the D6 fleet probe only enumerates demos that the backend can serve.
Move gen-ui-interrupt + interrupt-headless from features: to
not_supported_features: across affected integration manifests, and align
the generate-registry/generate-catalog scripts tests to the resulting
wired-feature counts (derive expected lengths from the parsed manifest
rather than hardcoding pre-quarantine numbers).
Bump canonicalCopilotKitVersion 1.59.2 -> 1.59.4 and pin every
integration's @copilotkit/* to 1.59.4 (locks regenerated). Keeps the
whole showcase on one version instead of letting the langgraph A2UI
demos deviate. Existing per-slug overrides (built-in-agent pkg.pr.new,
ms-agent-harness-dotnet 1.57.2) unchanged.