[DONT MERGE] test: regression — useAgent({ agentId }) under CopilotChatConfigurationProvider stops being a singleton by mme · Pull Request #4195 · CopilotKit/CopilotKit · GitHub
Skip to content

[DONT MERGE] test: regression — useAgent({ agentId }) under CopilotChatConfigurationProvider stops being a singleton#4195

Open
mme wants to merge 1 commit intomainfrom
test/useagent-singleton-regression
Open

[DONT MERGE] test: regression — useAgent({ agentId }) under CopilotChatConfigurationProvider stops being a singleton#4195
mme wants to merge 1 commit intomainfrom
test/useagent-singleton-regression

Conversation

@mme
Copy link
Copy Markdown
Contributor

@mme mme commented Apr 23, 2026

Summary

  • Two new tests in use-agent-thread-isolation.test.tsx. One is intentionally failing — it pins a real regression and must not be resolved by editing the test. Committed with --no-verify to preserve the failure.
  • Do not merge. This PR exists to document the bug and serve as a stable reproducer.

What broke

useAgent({ agentId }) used to return the registry singleton — one object shared across the entire app for a given agentId. The hook still advertises that behavior when no threadId is passed:

// packages/react-core/src/v2/hooks/use-agent.tsx
if (!threadId) {
  // No threadId — return the shared registry agent (original behavior)
  return existing;
}

But a few lines earlier the hook silently adopts a threadId from any surrounding CopilotChatConfigurationProvider:

const chatConfig = useCopilotChatConfiguration();
threadId ??= chatConfig?.threadId;

<CopilotChat> always installs that provider for its children (and auto-mints a UUID when no threadId prop is given). So anywhere inside a chat tree, a useAgent({ agentId }) call:

  • never reaches the if (!threadId) return existing branch,
  • receives a per-thread clone instead of the registry agent,
  • no longer shares identity with copilotkit.getAgent(agentId) or with a useAgent({ agentId }) call rendered outside the provider.

Net effect: the documented "original behavior" branch is effectively dead code in real apps, and state mutations made through the registry agent (or through a useAgent call outside the chat tree) are invisible to callers inside it.

What the tests prove

  1. BROKEN: useAgent({ agentId }) under CopilotChatConfigurationProvider returns a clone, not the registry singleton — failing. Renders two trackers, one inside a CopilotChatConfigurationProvider and one outside, both calling useAgent({ agentId }) with no threadId. Asserts:

    • outside caller equals registeredAgent
    • inside caller equals registeredAgent ✗ (fails — it's a clone with threadId: "conversation-1")
    • inside and outside callers are the same object ✗ (never reached)
  2. two useAgent({ agentId }) calls under the same provider share one clone — passing. Confirms identity is consistent inside a single provider: both callers resolve to the same clone via the module-level globalThreadCloneMap. This rules out "multiple clones per provider" as an explanation for the break in test (1) and isolates the regression to the provider boundary itself.

Repro

pnpm --filter @copilotkit/react-core exec vitest run \
  src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx

Expected output: 11 tests, 1 failed. Diff on the failing assertion:

-   "threadId": "<auto-minted UUID>",   ← registeredAgent (expected)
+   "threadId": "conversation-1",        ← clone returned (received)

Test plan

  • Do not merge.
  • Treat the failing test as the open ticket. When the no-threadId singleton path is restored, flip the failing assertion to passing to confirm the fix, and close this PR.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 23, 2026

@github-actions
Copy link
Copy Markdown
Contributor

📣 Social Copy Generator

Generate social media copies (Twitter/X, LinkedIn, Blog Post) for this PR using Claude.

  • Generate social media copies

…ConfigurationProvider returns a clone, not the registry singleton

useAgent is documented to preserve the original singleton behavior when no
threadId is passed, but `threadId ??= chatConfig?.threadId` silently pulls a
threadId from any surrounding CopilotChatConfigurationProvider (which
<CopilotChat> always installs), routing the caller onto the clone path. The
`if (!threadId) return existing` branch becomes dead code in that tree.

Adds one failing test that pins the identity break and one passing test
confirming that callers under the same provider at least converge on the
same clone via the module-level globalThreadCloneMap.

The failing test is intentional — committed with --no-verify so the failure
is preserved as a tracked regression until the no-threadId singleton path is
restored.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 23, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant