@copilotkit/react-native

中文文档
1.86.0发布于 2026/11/11MITTypeScriptESM + CJS构建来源已验证近 30 天下载 15.1万

Headless React Native bindings for CopilotKit. Provides a lightweight provider and re-exports platform-agnostic hooks -- no DOM, CSS, or web framework dependencies. You build the chat UI with standard React Native components.

原始使用文档
文档目录6 节
  1. Installation
  2. Polyfills
  3. Quick start
  4. Generative UI (experimental)
  5. API Surface
  6. Documentation

@copilotkit/react-native

Headless React Native bindings for CopilotKit. Provides a lightweight provider and re-exports platform-agnostic hooks -- no DOM, CSS, or web framework dependencies. You build the chat UI with standard React Native components.

Installation

npm install @copilotkit/react-native "zod@^3.25"

Every entry needs zod 3.25 or later, a peer dependency of @copilotkit/react-core. The command installs zod 3, which a generative UI catalog that uses child() or childList() needs.

Polyfills

React Native's JS runtime (Hermes) lacks several Web APIs that CopilotKit depends on. Import the polyfills before any other code in your entry point:

// index.js
import "@copilotkit/react-native/polyfills";

import { AppRegistry } from "react-native";
import App from "./App";
import { name as appName } from "./app.json";

AppRegistry.registerComponent(appName, () => App);

If you already polyfill some of these APIs, you can import only what you need:

import "@copilotkit/react-native/polyfills/streams";
import "@copilotkit/react-native/polyfills/encoding";
import "@copilotkit/react-native/polyfills/crypto";
import "@copilotkit/react-native/polyfills/dom";
import "@copilotkit/react-native/polyfills/location";

Quick start

import {
  CopilotKitProvider,
  useAgent,
  useCopilotKit,
} from "@copilotkit/react-native/headless";

export default function App() {
  return (
    <CopilotKitProvider runtimeUrl="https://your-server/api/copilotkit">
      <ChatScreen />
    </CopilotKitProvider>
  );
}

Re-exports hooks from @copilotkit/react-core: useAgent, useFrontendTool, useComponent, useHumanInTheLoop, useInterrupt, useSuggestions, useConfigureSuggestions, useAgentContext, useThreads, and useCopilotKit.

Generative UI (experimental)

@copilotkit/react-native/generative-ui lets the agent render a catalog of your own native components with the render_ui tool. Install zod 3.25 or later: every entry of this package needs it, because @copilotkit/react-core requires it as a peer dependency (@copilotkit/react-native declares zod an optional peer at >=3.0.0 only because narrowing that range would be a breaking change). Install zod 3 (npm install "zod@^3.25") for a catalog that uses child() or childList(): they return zod 3 schemas, which a zod 4 schema cannot hold. The runtime needs no change by default: CopilotRuntime adds the tool when the app sends a catalog with at least one component, unless the server sets generativeUI: false or limits it with generativeUI: { agents: [...] }. This example needs a home agent on the runtime: once the runtime has connected, useAgent throws without it. /generative-ui is not in 1.85.0 or any earlier version; it first ships in the release after 1.85.0.

import { useCallback, useEffect, useRef, useState } from "react";
import {
  ActivityIndicator,
  Pressable,
  ScrollView,
  Text,
  View,
} from "react-native";
import { z } from "zod";
import {
  CopilotKitCoreErrorCode,
  CopilotKitCoreRuntimeConnectionStatus,
  CopilotKitProvider,
  useAgent,
  useAgentContext,
  useCopilotKit,
} from "@copilotkit/react-native/headless";
import {
  GenerativeUIActivity,
  GenerativeUIProvider,
  createGenerativeUI,
  isGenerativeUIActivity,
  useGenerativeUIRuntime,
  type GenerativeUIActivityMessage,
} from "@copilotkit/react-native/generative-ui";

const ui = createGenerativeUI({
  id: "acme/bank@1",
  components: {
    BalanceCard: {
      component: ({ account, amount }: { account: string; amount: number }) => (
        <View>
          <Text>{account}</Text>
          <Text>{amount}</Text>
        </View>
      ),
      description:
        "The balance of one account. Copy the values from the context.",
      props: z.object({ account: z.string(), amount: z.number() }),
    },
  },
});

type CopilotKit = ReturnType<typeof useCopilotKit>["copilotkit"];
type Agent = ReturnType<typeof useAgent>["agent"];

/**
 * The errors that end a run. A failed tool call is not one of them
 * (TOOL_HANDLER_FAILED, TOOL_ARGUMENT_PARSE_FAILED): its error goes back to
 * the model as the result of the call, and the run goes on.
 */
const RUN_FAILURES: ReadonlySet<CopilotKitCoreErrorCode> = new Set([
  CopilotKitCoreErrorCode.AGENT_RUN_FAILED,
  CopilotKitCoreErrorCode.AGENT_RUN_FAILED_EVENT,
  CopilotKitCoreErrorCode.AGENT_RUN_ERROR_EVENT,
  CopilotKitCoreErrorCode.AGENT_THREAD_LOCKED,
]);

// Date.now() alone repeats within a millisecond; with a counter, ids do not.
let sent = 0;

/**
 * Sends one user message and runs the agent. Resolves with the run's error,
 * or undefined; it never rejects. It counts every run failure of this agent
 * while it waits, so start one run of an agent at a time.
 */
async function runTurn(copilotkit: CopilotKit, agent: Agent, content: string) {
  const before = agent.messages.map((message) => message.id);
  const { threadId } = agent;
  let failure: Error | undefined;
  const subscription = copilotkit.subscribe({
    onError: ({ error, code, context }) => {
      if (context.agentId === agent.agentId && RUN_FAILURES.has(code)) {
        failure ??= error;
      }
    },
  });
  try {
    sent += 1;
    agent.addMessage({
      id: `user-${Date.now()}-${sent}`,
      role: "user",
      content,
    });
    await copilotkit.runAgent({ agent });
  } catch (error) {
    failure ??= error instanceof Error ? error : new Error(String(error));
  } finally {
    subscription.unsubscribe();
  }
  try {
    if (failure) takeBack(agent, before, threadId);
  } catch (error) {
    console.error("Could not take the failed turn back:", error);
  }
  return failure;
}

/**
 * Removes the messages added since `before` (their ids), such as the user
 * message and a partial reply. It keeps the surfaces: the provider keeps
 * every surface that a run rendered, and the model still sees it, so the
 * screen shows it too.
 */
function takeBack(
  agent: Agent,
  before: readonly string[],
  threadId: string | undefined,
) {
  // The agent moved to another thread: those messages are not this turn's.
  if (agent.threadId !== threadId) return;
  const kept = new Set(before);
  agent.setMessages(
    agent.messages.filter(
      (message) => kept.has(message.id) || isGenerativeUIActivity(message),
    ),
  );
}

/**
 * An activity message that GenerativeUIActivity draws: its surface is aimed at
 * the chat (the target it shows) and the agent has not deleted it.
 */
function isChatSurface(message: GenerativeUIActivityMessage) {
  if (!isGenerativeUIActivity(message)) return false;
  const content = message.content as {
    target?: unknown;
    operations?: unknown;
  } | null;
  const operations = Array.isArray(content?.operations)
    ? content.operations
    : [];
  return (
    content?.target === "chat" &&
    !operations.some(
      (op) => typeof op === "object" && op !== null && "deleteSurface" in op,
    )
  );
}

/**
 * The chat surfaces that this device built, each message id with its operations,
 * to tell whether a run rendered or updated one here. The operations alone: each
 * call of the agent also refills the list of rejected operations, so a call whose
 * updates were all rejected changes the content but not the surface. A surface
 * whose createSurface the app rejected (`failed`) does not count: the app checks
 * the operations again, prop values included, and GenerativeUIActivity then shows
 * only an error line.
 */
function chatSurfaces(
  messages: readonly GenerativeUIActivityMessage[],
  failed: (surfaceId: string) => boolean,
) {
  const built = new Map<string | undefined, string>();
  for (const message of messages.filter(isChatSurface)) {
    const { surfaceId, operations } = message.content as {
      surfaceId?: unknown;
      operations?: unknown;
    };
    if (typeof surfaceId === "string" && failed(surfaceId)) continue;
    built.set(message.id, JSON.stringify(operations));
  }
  return built;
}

// Your app's data. The agent sees only what you give it with useAgentContext.
const accounts = [
  { account: "Chequing", amount: 3482.1 },
  { account: "Savings", amount: 6150 },
];

function Home() {
  const { copilotkit } = useCopilotKit();
  // Until the runtime connects, `agent` is a stand-in (`isReady` is false). The
  // registered agent replaces it, so a run started on the stand-in is lost.
  // Once the runtime has connected, useAgent throws if it has no `home` agent.
  const { agent, isReady } = useAgent({ agentId: "home" });
  const runtime = useGenerativeUIRuntime();
  // The thread it last checked: a ref, so StrictMode's second effect run sees it.
  const checkedThread = useRef<string | undefined>(undefined);
  const composing = useRef(false); // a ref, so a second tap is ignored at once
  const [checked, setChecked] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string>();

  useAgentContext({
    description:
      "The user's accounts. Show only these, with these exact balances.",
    value: accounts,
  });

  const failed = useCallback(
    (surfaceId: string) =>
      !!runtime?.manager.getFailedSurface(agent.threadId, surfaceId),
    [runtime, agent],
  );

  const compose = useCallback(async () => {
    // A stand-in agent (not ready) would lose the run.
    if (composing.current || !isReady) return;
    composing.current = true;
    setBusy(true);
    setError(undefined);
    try {
      const before = agent.messages.map((message) => message.id);
      const { threadId } = agent;
      const shownBefore = chatSurfaces(agent.messages, failed);
      const failure = await runTurn(copilotkit, agent, "Show my balances.");
      const shownAfter = chatSurfaces(agent.messages, failed);
      if (failure) {
        // runTurn took the prompt back, so a retry sends it once.
        setError(failure.message);
      } else if (
        ![...shownAfter].some(
          ([id, content]) => shownBefore.get(id) !== content,
        )
      ) {
        // This run rendered or updated nothing here. Take its prompt back too.
        takeBack(agent, before, threadId);
        setError("The agent rendered nothing for this screen.");
      }
    } catch (reason) {
      setError(reason instanceof Error ? reason.message : String(reason));
    } finally {
      composing.current = false;
      setBusy(false);
    }
  }, [agent, copilotkit, failed, isReady]);

  // The agent and its messages live in the core and outlive this screen. Run only
  // when the registered agent has no surface here yet and is not running, so a
  // screen that mounts again shows what it has instead of composing it again.
  // Check again when the agent moves to another thread, once it is not running
  // and no compose is under way (`busy` reruns this when one ends).
  useEffect(() => {
    if (
      !isReady ||
      busy ||
      composing.current ||
      agent.isRunning ||
      checkedThread.current === agent.threadId
    ) {
      return;
    }
    checkedThread.current = agent.threadId;
    setChecked(true);
    setError(undefined);
    if (chatSurfaces(agent.messages, failed).size === 0) {
      void compose();
    }
  }, [isReady, busy, agent, agent.threadId, agent.isRunning, compose, failed]);

  const surfaces = agent.messages.filter(isChatSurface);
  const built = chatSurfaces(agent.messages, failed);
  const unreachable =
    copilotkit.runtimeConnectionStatus ===
    CopilotKitCoreRuntimeConnectionStatus.Error;
  const waiting = !isReady || !checked || busy || agent.isRunning;

  return (
    <ScrollView contentContainerStyle={{ padding: 16 }}>
      {surfaces.map((message) => (
        <GenerativeUIActivity
          key={message.id}
          message={message}
          agent={agent}
        />
      ))}
      {unreachable ? (
        <Text accessibilityRole="alert">
          Can't reach the agent. Check the runtime URL.
        </Text>
      ) : waiting ? (
        <ActivityIndicator />
      ) : error || built.size === 0 ? (
        <Pressable accessibilityRole="button" onPress={() => void compose()}>
          <Text>
            Couldn't load your balances{error ? ` (${error})` : ""}. Tap to try
            again.
          </Text>
        </Pressable>
      ) : null}
    </ScrollView>
  );
}

export default function App() {
  return (
    <CopilotKitProvider runtimeUrl="https://your-server/api/copilotkit">
      <GenerativeUIProvider ui={ui}>
        <Home />
      </GenerativeUIProvider>
    </CopilotKitProvider>
  );
}

GenerativeUIActivity renders the surface of one activity message, in your own message list or on a screen with no chat. Like the web chat, it shows only surfaces aimed at the chat; pass target to show another target. It renders nothing (and warns in development) without a GenerativeUIProvider, under a provider without ui, or while the agent has no threadId. The example shows a spinner, an unreachable runtime and a retry with the reason. A failed run's prompt is taken back, but the surfaces it rendered stay: the provider and the model keep them. The docs add a chat list. GenerativeUISurface marks a named place in the app; style applies while its fallback shows too. Tool transport only; there is no standard catalog, no theme prop (a theme passed from JavaScript is dropped with a development warning) and no openUrl (it fails with "openUrl is not available on this platform"), and the prebuilt CopilotChat does not draw surfaces yet. See the React Native docs.

API Surface

The root import (@copilotkit/react-native) IS the v2 API. Unlike the web SDK which has /v2 subpath, React Native exports the v2 API directly from the package root.

The root also re-exports the chat components and useAttachments, which imports expo-document-picker and expo-file-system; Metro resolves them at bundle time, so an app that imports from the root must install both. @copilotkit/react-native/headless exports the provider and hooks without them, which is why the Quick Start above uses it.

Documentation

For full setup instructions, usage examples, and troubleshooting, see the React Native docs.

版本与发布记录

@copilotkit/react-native 共发布 160 个版本,首次发布于 2026/05/07;过去 12 个月发布了 160 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.86.0latest2026/11/11今天
2.0.0-rc.40next2026/11/11今天
2.0.0-rc.392026/11/10今天
2.0.0-rc.382026/11/10今天
2.0.0-rc.372026/11/091 天前
2.0.0-rc.362026/11/064 天前

其他发布通道:canary → 1.84.2-canary.openbot-intel-fixes,next → 2.0.0-rc.40。

关于 @copilotkit/react-native 的常见问题

@copilotkit/react-native 是什么?

Headless React Native bindings for CopilotKit. Provides a lightweight provider and re-exports platform-agnostic hooks -- no DOM, CSS, or web framework dependencies. You build the chat UI with standard React Native components.

如何安装 @copilotkit/react-native?

在项目目录运行 npm install @copilotkit/react-native,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^18 || ^19)、react-native(>=0.70)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@copilotkit/react-native 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@copilotkit/react-native 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@copilotkit/react-native 可以商用吗?

@copilotkit/react-native 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@copilotkit/react-native 的下载量有多少?

根据 npm 官方下载统计,@copilotkit/react-native 在 2026/10/11 – 2026/11/09 这 30 天内被下载 150,581 次,日均约 5,019 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@copilotkit/react-native 还在维护吗?

最新版本 1.86.0 发布于 2026/11/11(今天);至今共发布 160 个版本,其中过去 12 个月发布了 160 个;首次发布于 2026/05/07。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。