UNPKG

@copilotkit/react-core

Version:

<img src="https://github.com/user-attachments/assets/0a6b64d9-e193-4940-a3f6-60334ac34084" alt="banner" style="border-radius: 12px; border: 2px solid #d6d4fa;" />

1,137 lines (1,122 loc) 37.1 kB
import { CopilotKitCoreReact, useCopilotKit } from "@copilotkit/react-core/v2/context"; import React, { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react"; import { DEFAULT_AGENT_ID, randomUUID } from "@copilotkit/shared"; import { twMerge } from "tailwind-merge"; import { jsx } from "react/jsx-runtime"; import { HttpAgent } from "@ag-ui/client"; import { CopilotKitCoreRuntimeConnectionStatus, ProxiedCopilotRuntimeAgent, ɵcreateThreadStore, ɵselectHasNextPage, ɵselectIsFetchingNextPage, ɵselectThreads, ɵselectThreadsError, ɵselectThreadsIsLoading } from "@copilotkit/core"; import { z } from "zod"; //#region src/v2/lib/slots.tsx /** * Shallow equality comparison for objects. */ function shallowEqual(obj1, obj2) { const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2); if (keys1.length !== keys2.length) return false; for (const key of keys1) if (obj1[key] !== obj2[key]) return false; return true; } /** * Returns true only for plain JS objects (`{}`), excluding arrays, Dates, * class instances, and other exotic objects that happen to have typeof "object". */ function isPlainObject(obj) { return obj !== null && typeof obj === "object" && Object.prototype.toString.call(obj) === "[object Object]"; } /** * Returns the same reference as long as the value is shallowly equal to the * previous render's value. * * - Identical references bail out immediately (O(1)). * - Plain objects ({}) are shallow-compared key-by-key. * - Arrays, Dates, class instances, functions, and primitives are compared by * reference only — shallowEqual is never called on non-plain objects, which * avoids incorrect equality for e.g. [1,2] vs [1,2] (different arrays). * * Typical use: stabilize inline slot props so MemoizedSlotWrapper's shallow * equality check isn't defeated by a new object reference on every render. */ function useShallowStableRef(value) { const ref = useRef(value); if (ref.current === value) return ref.current; if (isPlainObject(ref.current) && isPlainObject(value)) { if (shallowEqual(ref.current, value)) return ref.current; } ref.current = value; return ref.current; } /** * Check if a value is a React component type (function, class, forwardRef, memo, etc.) */ function isReactComponentType(value) { if (typeof value === "function") return true; if (value && typeof value === "object" && "$$typeof" in value && !React.isValidElement(value)) return true; return false; } /** * Internal function to render a slot value as a React element (non-memoized). */ function renderSlotElement(slot, DefaultComponent, props) { if (typeof slot === "string") { const existingClassName = props.className; return React.createElement(DefaultComponent, { ...props, className: twMerge(existingClassName, slot) }); } if (isReactComponentType(slot)) return React.createElement(slot, props); if (slot && typeof slot === "object" && !React.isValidElement(slot)) return React.createElement(DefaultComponent, { ...props, ...slot }); return React.createElement(DefaultComponent, props); } /** * Internal memoized wrapper component for renderSlot. * Uses forwardRef to support ref forwarding. */ const MemoizedSlotWrapper = React.memo(React.forwardRef(function MemoizedSlotWrapper(props, ref) { const { $slot, $component, ...rest } = props; return renderSlotElement($slot, $component, ref !== null ? { ...rest, ref } : rest); }), (prev, next) => { if (prev.$slot !== next.$slot) return false; if (prev.$component !== next.$component) return false; const { $slot: _ps, $component: _pc, ...prevRest } = prev; const { $slot: _ns, $component: _nc, ...nextRest } = next; return shallowEqual(prevRest, nextRest); }); //#endregion //#region src/v2/providers/CopilotChatConfigurationProvider.tsx const CopilotChatDefaultLabels = { chatInputPlaceholder: "Type a message...", chatInputToolbarStartTranscribeButtonLabel: "Transcribe", chatInputToolbarCancelTranscribeButtonLabel: "Cancel", chatInputToolbarFinishTranscribeButtonLabel: "Finish", chatInputToolbarAddButtonLabel: "Add attachments", chatInputToolbarToolsButtonLabel: "Tools", assistantMessageToolbarCopyCodeLabel: "Copy", assistantMessageToolbarCopyCodeCopiedLabel: "Copied", assistantMessageToolbarCopyMessageLabel: "Copy", assistantMessageToolbarThumbsUpLabel: "Good response", assistantMessageToolbarThumbsDownLabel: "Bad response", assistantMessageToolbarReadAloudLabel: "Read aloud", assistantMessageToolbarRegenerateLabel: "Regenerate", userMessageToolbarCopyMessageLabel: "Copy", userMessageToolbarEditMessageLabel: "Edit", chatDisclaimerText: "AI can make mistakes. Please verify important information.", chatToggleOpenLabel: "Open chat", chatToggleCloseLabel: "Close chat", modalHeaderTitle: "CopilotKit Chat", welcomeMessageText: "How can I help you today?" }; const CopilotChatConfiguration = createContext(null); const CopilotChatConfigurationProvider = ({ children, labels, agentId, threadId, hasExplicitThreadId, isModalDefaultOpen }) => { const parentConfig = useContext(CopilotChatConfiguration); const stableLabels = useShallowStableRef(labels); const mergedLabels = useMemo(() => ({ ...CopilotChatDefaultLabels, ...parentConfig?.labels, ...stableLabels }), [stableLabels, parentConfig?.labels]); const resolvedAgentId = agentId ?? parentConfig?.agentId ?? DEFAULT_AGENT_ID; const resolvedThreadId = useMemo(() => { if (threadId) return threadId; if (parentConfig?.threadId) return parentConfig.threadId; return randomUUID(); }, [threadId, parentConfig?.threadId]); const resolvedHasExplicitThreadId = (hasExplicitThreadId !== void 0 ? hasExplicitThreadId : !!threadId) || !!parentConfig?.hasExplicitThreadId; const [internalModalOpen, setInternalModalOpen] = useState(isModalDefaultOpen ?? true); const hasExplicitDefault = isModalDefaultOpen !== void 0; const setAndSync = useCallback((open) => { setInternalModalOpen(open); parentConfig?.setModalOpen(open); }, [parentConfig?.setModalOpen]); const isMounted = useRef(false); useEffect(() => { if (!hasExplicitDefault) return; if (!isMounted.current) { isMounted.current = true; return; } if (parentConfig?.isModalOpen === void 0) return; setInternalModalOpen(parentConfig.isModalOpen); }, [parentConfig?.isModalOpen, hasExplicitDefault]); const resolvedIsModalOpen = hasExplicitDefault ? internalModalOpen : parentConfig?.isModalOpen ?? internalModalOpen; const resolvedSetModalOpen = hasExplicitDefault ? setAndSync : parentConfig?.setModalOpen ?? setInternalModalOpen; const configurationValue = useMemo(() => ({ labels: mergedLabels, agentId: resolvedAgentId, threadId: resolvedThreadId, hasExplicitThreadId: resolvedHasExplicitThreadId, isModalOpen: resolvedIsModalOpen, setModalOpen: resolvedSetModalOpen }), [ mergedLabels, resolvedAgentId, resolvedThreadId, resolvedHasExplicitThreadId, resolvedIsModalOpen, resolvedSetModalOpen ]); return /* @__PURE__ */ jsx(CopilotChatConfiguration.Provider, { value: configurationValue, children }); }; const useCopilotChatConfiguration = () => { return useContext(CopilotChatConfiguration); }; //#endregion //#region src/v2/hooks/use-agent.tsx let UseAgentUpdate = /* @__PURE__ */ function(UseAgentUpdate) { UseAgentUpdate["OnMessagesChanged"] = "OnMessagesChanged"; UseAgentUpdate["OnStateChanged"] = "OnStateChanged"; UseAgentUpdate["OnRunStatusChanged"] = "OnRunStatusChanged"; return UseAgentUpdate; }({}); const ALL_UPDATES = [ UseAgentUpdate.OnMessagesChanged, UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged ]; function useAgent({ agentId, updates, throttleMs } = {}) { agentId ??= DEFAULT_AGENT_ID; const { copilotkit } = useCopilotKit(); const providerThrottleMs = copilotkit.defaultThrottleMs; const [, forceUpdate] = useReducer((x) => x + 1, 0); const updateFlags = useMemo(() => updates ?? ALL_UPDATES, [JSON.stringify(updates)]); const provisionalAgentCache = useRef(/* @__PURE__ */ new Map()); const agent = useMemo(() => { const existing = copilotkit.getAgent(agentId); if (existing) { provisionalAgentCache.current.delete(agentId); return existing; } const isRuntimeConfigured = copilotkit.runtimeUrl !== void 0; const status = copilotkit.runtimeConnectionStatus; if (isRuntimeConfigured && (status === CopilotKitCoreRuntimeConnectionStatus.Disconnected || status === CopilotKitCoreRuntimeConnectionStatus.Connecting)) { const cached = provisionalAgentCache.current.get(agentId); if (cached) { cached.headers = { ...copilotkit.headers }; return cached; } const provisional = new ProxiedCopilotRuntimeAgent({ runtimeUrl: copilotkit.runtimeUrl, agentId, transport: copilotkit.runtimeTransport, runtimeMode: "pending" }); provisional.headers = { ...copilotkit.headers }; provisionalAgentCache.current.set(agentId, provisional); return provisional; } if (isRuntimeConfigured && status === CopilotKitCoreRuntimeConnectionStatus.Error) { const cached = provisionalAgentCache.current.get(agentId); if (cached) { cached.headers = { ...copilotkit.headers }; return cached; } const provisional = new ProxiedCopilotRuntimeAgent({ runtimeUrl: copilotkit.runtimeUrl, agentId, transport: copilotkit.runtimeTransport, runtimeMode: "pending" }); provisional.headers = { ...copilotkit.headers }; provisionalAgentCache.current.set(agentId, provisional); return provisional; } const knownAgents = Object.keys(copilotkit.agents ?? {}); const runtimePart = isRuntimeConfigured ? `runtimeUrl=${copilotkit.runtimeUrl}` : "no runtimeUrl"; throw new Error(`useAgent: Agent '${agentId}' not found after runtime sync (${runtimePart}). ` + (knownAgents.length ? `Known agents: [${knownAgents.join(", ")}]` : "No agents registered.") + " Verify your runtime /info and/or agents__unsafe_dev_only."); }, [ agentId, copilotkit.agents, copilotkit.runtimeConnectionStatus, copilotkit.runtimeUrl, copilotkit.runtimeTransport, JSON.stringify(copilotkit.headers) ]); useEffect(() => { if (updateFlags.length === 0) return; let active = true; const handlers = {}; let batchScheduled = false; const batchedForceUpdate = () => { if (!active) return; if (!batchScheduled) { batchScheduled = true; queueMicrotask(() => { batchScheduled = false; if (active) forceUpdate(); }); } }; if (updateFlags.includes(UseAgentUpdate.OnMessagesChanged)) handlers.onMessagesChanged = batchedForceUpdate; if (updateFlags.includes(UseAgentUpdate.OnStateChanged)) handlers.onStateChanged = batchedForceUpdate; if (updateFlags.includes(UseAgentUpdate.OnRunStatusChanged)) { handlers.onRunInitialized = batchedForceUpdate; handlers.onRunFinalized = batchedForceUpdate; handlers.onRunFailed = batchedForceUpdate; handlers.onRunErrorEvent = batchedForceUpdate; } const subscription = copilotkit.subscribeToAgentWithOptions(agent, handlers, { throttleMs }); return () => { active = false; subscription.unsubscribe(); }; }, [ agent, forceUpdate, throttleMs, providerThrottleMs, updateFlags ]); useEffect(() => { if (agent instanceof HttpAgent) agent.headers = { ...copilotkit.headers }; }, [agent, JSON.stringify(copilotkit.headers)]); const chatConfig = useCopilotChatConfiguration(); const configThreadId = chatConfig?.threadId; const configHasExplicitThreadId = chatConfig?.hasExplicitThreadId; useEffect(() => { if (!configHasExplicitThreadId || !configThreadId) return; agent.threadId = configThreadId; }, [ agent, configThreadId, configHasExplicitThreadId ]); return { agent }; } //#endregion //#region src/v2/hooks/use-frontend-tool.tsx const EMPTY_DEPS$1 = []; function useFrontendTool(tool, deps) { const { copilotkit } = useCopilotKit(); const extraDeps = deps ?? EMPTY_DEPS$1; useEffect(() => { const name = tool.name; if (copilotkit.getTool({ toolName: name, agentId: tool.agentId })) { console.warn(`Tool '${name}' already exists for agent '${tool.agentId || "global"}'. Overriding with latest registration.`); copilotkit.removeTool(name, tool.agentId); } copilotkit.addTool(tool); if (tool.render) copilotkit.addHookRenderToolCall({ name, args: tool.parameters, agentId: tool.agentId, render: tool.render }); return () => { copilotkit.removeTool(name, tool.agentId); }; }, [ tool.name, tool.available, copilotkit, JSON.stringify(extraDeps) ]); } //#endregion //#region src/v2/hooks/use-component.tsx /** * Registers a React component as a frontend tool renderer in chat. * * This hook is a convenience wrapper around `useFrontendTool` that: * - builds a model-facing tool description, * - forwards optional schema parameters (any Standard Schema V1 compatible library), * - renders your component with tool call parameters. * * Use this when you want to display a typed visual component for a tool call * without manually wiring a full frontend tool object. * * When `parameters` is provided, render props are inferred from the schema. * When omitted, the render component may accept any props. * * @typeParam TSchema - Schema describing tool parameters, or `undefined` when no schema is given. * @param config - Tool registration config. * @param deps - Optional dependencies to refresh registration (same semantics as `useEffect`). * * @example * ```tsx * // Without parameters — render accepts any props * useComponent({ * name: "showGreeting", * render: ({ message }: { message: string }) => <div>{message}</div>, * }); * ``` * * @example * ```tsx * // With parameters — render props inferred from schema * useComponent({ * name: "showWeatherCard", * parameters: z.object({ city: z.string() }), * render: ({ city }) => <div>{city}</div>, * }); * ``` * * @example * ```tsx * useComponent( * { * name: "renderProfile", * parameters: z.object({ userId: z.string() }), * render: ProfileCard, * agentId: "support-agent", * }, * [selectedAgentId], * ); * ``` */ function useComponent(config, deps) { const prefix = `Use this tool to display the "${config.name}" component in the chat. This tool renders a visual UI component for the user.`; const fullDescription = config.description ? `${prefix}\n\n${config.description}` : prefix; useFrontendTool({ name: config.name, description: fullDescription, parameters: config.parameters, render: ({ args }) => { const Component = config.render; return /* @__PURE__ */ jsx(Component, { ...args }); }, agentId: config.agentId, followUp: config.followUp }, deps); } //#endregion //#region src/v2/hooks/use-human-in-the-loop.tsx function useHumanInTheLoop(tool, deps) { const { copilotkit } = useCopilotKit(); const resolvePromiseRef = useRef(null); const respond = useCallback(async (result) => { if (resolvePromiseRef.current) { resolvePromiseRef.current(result); resolvePromiseRef.current = null; } }, []); const handler = useCallback(async () => { return new Promise((resolve) => { resolvePromiseRef.current = resolve; }); }, []); const RenderComponent = useCallback((props) => { const ToolComponent = tool.render; if (props.status === "inProgress") { const enhancedProps = { ...props, name: tool.name, description: tool.description || "", respond: void 0 }; return React.createElement(ToolComponent, enhancedProps); } else if (props.status === "executing") { const enhancedProps = { ...props, name: tool.name, description: tool.description || "", respond }; return React.createElement(ToolComponent, enhancedProps); } else if (props.status === "complete") { const enhancedProps = { ...props, name: tool.name, description: tool.description || "", respond: void 0 }; return React.createElement(ToolComponent, enhancedProps); } return React.createElement(ToolComponent, props); }, [ tool.render, tool.name, tool.description, respond ]); useFrontendTool({ ...tool, handler, render: RenderComponent }, deps); useEffect(() => { return () => { copilotkit.removeHookRenderToolCall(tool.name, tool.agentId); }; }, [ copilotkit, tool.name, tool.agentId ]); } //#endregion //#region src/v2/hooks/use-interrupt.tsx const INTERRUPT_EVENT_NAME = "on_interrupt"; function isPromiseLike(value) { return (typeof value === "object" || typeof value === "function") && value !== null && typeof Reflect.get(value, "then") === "function"; } /** * Handles agent interrupts (`on_interrupt`) with optional filtering, preprocessing, and resume behavior. * * The hook listens to custom events on the active agent, stores interrupt payloads per run, * and surfaces a render callback once the run finalizes. Call `resolve` from your UI to resume * execution with user-provided data. * * - `renderInChat: true` (default): the element is published into `<CopilotChat>` and this hook returns `void`. * - `renderInChat: false`: the hook returns the interrupt element so you can place it anywhere in your component tree. * * `event.value` is typed as `any` since the interrupt payload shape depends on your agent. * Type-narrow it in your callbacks (e.g. `handler`, `enabled`, `render`) as needed. * * @typeParam TResult - Inferred from `handler` return type. Exposed as `result` in `render`. * @param config - Interrupt configuration (renderer, optional handler/filter, and render mode). * @returns When `renderInChat` is `false`, returns the interrupt element (or `null` when idle). * Otherwise returns `void` and publishes the element into chat. In `render`, `result` is always * either the handler's resolved return value or `null` (including when no handler is provided, * when filtering skips the interrupt, or when handler execution fails). * * @example * ```tsx * import { useInterrupt } from "@copilotkit/react-core/v2"; * * function InterruptUI() { * useInterrupt({ * render: ({ event, resolve }) => ( * <div> * <p>{event.value.question}</p> * <button onClick={() => resolve({ approved: true })}>Approve</button> * <button onClick={() => resolve({ approved: false })}>Reject</button> * </div> * ), * }); * * return null; * } * ``` * * @example * ```tsx * import { useInterrupt } from "@copilotkit/react-core/v2"; * * function CustomPanel() { * const interruptElement = useInterrupt({ * renderInChat: false, * enabled: (event) => event.value.startsWith("approval:"), * handler: async ({ event }) => ({ label: event.value.toUpperCase() }), * render: ({ event, result, resolve }) => ( * <aside> * <strong>{result?.label ?? ""}</strong> * <button onClick={() => resolve({ value: event.value })}>Continue</button> * </aside> * ), * }); * * return <>{interruptElement}</>; * } * ``` */ function useInterrupt(config) { const { copilotkit } = useCopilotKit(); const { agent } = useAgent({ agentId: config.agentId }); const [pendingEvent, setPendingEvent] = useState(null); const pendingEventRef = useRef(pendingEvent); pendingEventRef.current = pendingEvent; const [handlerResult, setHandlerResult] = useState(null); useEffect(() => { let localInterrupt = null; const subscription = agent.subscribe({ onCustomEvent: ({ event }) => { if (event.name === INTERRUPT_EVENT_NAME) localInterrupt = { name: event.name, value: event.value }; }, onRunStartedEvent: () => { localInterrupt = null; setPendingEvent(null); }, onRunFinalized: () => { if (localInterrupt) { setPendingEvent(localInterrupt); localInterrupt = null; } }, onRunFailed: () => { localInterrupt = null; } }); return () => subscription.unsubscribe(); }, [agent]); const resolve = useCallback((response) => { copilotkit.runAgent({ agent, forwardedProps: { command: { resume: response, interruptEvent: pendingEventRef.current?.value } } }); }, [agent, copilotkit]); const renderRef = useRef(config.render); renderRef.current = config.render; const enabledRef = useRef(config.enabled); enabledRef.current = config.enabled; const handlerRef = useRef(config.handler); handlerRef.current = config.handler; const resolveRef = useRef(resolve); resolveRef.current = resolve; const isEnabled = (event) => { const predicate = enabledRef.current; if (!predicate) return true; try { return predicate(event); } catch (err) { console.error("[CopilotKit] useInterrupt enabled predicate threw; treating interrupt as disabled:", err); return false; } }; useEffect(() => { if (!pendingEvent) { setHandlerResult(null); return; } if (!isEnabled(pendingEvent)) { setHandlerResult(null); return; } const handler = handlerRef.current; if (!handler) { setHandlerResult(null); return; } let cancelled = false; let maybePromise; try { maybePromise = handler({ event: pendingEvent, resolve: resolveRef.current }); } catch (err) { console.error("[CopilotKit] useInterrupt handler threw; result will be null:", err); if (!cancelled) setHandlerResult(null); return () => { cancelled = true; }; } if (isPromiseLike(maybePromise)) Promise.resolve(maybePromise).then((resolved) => { if (!cancelled) setHandlerResult(resolved); }).catch((err) => { console.error("[CopilotKit] useInterrupt handler rejected; result will be null:", err); if (!cancelled) setHandlerResult(null); }); else setHandlerResult(maybePromise); return () => { cancelled = true; }; }, [pendingEvent]); const element = useMemo(() => { if (!pendingEvent) return null; if (!isEnabled(pendingEvent)) return null; return renderRef.current({ event: pendingEvent, result: handlerResult, resolve }); }, [ pendingEvent, handlerResult, resolve ]); useEffect(() => { if (config.renderInChat === false) return; copilotkit.setInterruptElement(element); }, [ element, config.renderInChat, copilotkit ]); useEffect(() => { if (config.renderInChat === false) return; return () => { copilotkit.setInterruptElement(null); }; }, []); if (config.renderInChat === false) return element; } //#endregion //#region src/v2/hooks/use-suggestions.tsx function useSuggestions({ agentId } = {}) { const { copilotkit } = useCopilotKit(); const config = useCopilotChatConfiguration(); const resolvedAgentId = useMemo(() => agentId ?? config?.agentId ?? DEFAULT_AGENT_ID, [agentId, config?.agentId]); const [suggestions, setSuggestions] = useState(() => { return copilotkit.getSuggestions(resolvedAgentId).suggestions; }); const [isLoading, setIsLoading] = useState(() => { return copilotkit.getSuggestions(resolvedAgentId).isLoading; }); useEffect(() => { const result = copilotkit.getSuggestions(resolvedAgentId); setSuggestions(result.suggestions); setIsLoading(result.isLoading); }, [copilotkit, resolvedAgentId]); useEffect(() => { const subscription = copilotkit.subscribe({ onSuggestionsChanged: ({ agentId: changedAgentId, suggestions }) => { if (changedAgentId !== resolvedAgentId) return; setSuggestions(suggestions); }, onSuggestionsStartedLoading: ({ agentId: changedAgentId }) => { if (changedAgentId !== resolvedAgentId) return; setIsLoading(true); }, onSuggestionsFinishedLoading: ({ agentId: changedAgentId }) => { if (changedAgentId !== resolvedAgentId) return; setIsLoading(false); }, onSuggestionsConfigChanged: () => { const result = copilotkit.getSuggestions(resolvedAgentId); setSuggestions(result.suggestions); setIsLoading(result.isLoading); } }); return () => { subscription.unsubscribe(); }; }, [copilotkit, resolvedAgentId]); return { suggestions, reloadSuggestions: useCallback(() => { copilotkit.reloadSuggestions(resolvedAgentId); }, [copilotkit, resolvedAgentId]), clearSuggestions: useCallback(() => { copilotkit.clearSuggestions(resolvedAgentId); }, [copilotkit, resolvedAgentId]), isLoading }; } //#endregion //#region src/v2/hooks/use-configure-suggestions.tsx function useConfigureSuggestions(config, deps) { const { copilotkit } = useCopilotKit(); const chatConfig = useCopilotChatConfiguration(); const extraDeps = deps ?? []; const resolvedConsumerAgentId = useMemo(() => chatConfig?.agentId ?? DEFAULT_AGENT_ID, [chatConfig?.agentId]); const rawConsumerAgentId = useMemo(() => config ? config.consumerAgentId : void 0, [config]); const normalizationCacheRef = useRef({ serialized: null, config: null }); const { normalizedConfig, serializedConfig } = useMemo(() => { if (!config) { normalizationCacheRef.current = { serialized: null, config: null }; return { normalizedConfig: null, serializedConfig: null }; } if (config.available === "disabled") { normalizationCacheRef.current = { serialized: null, config: null }; return { normalizedConfig: null, serializedConfig: null }; } let built; if (isDynamicConfig(config)) built = { ...config }; else { const normalizedSuggestions = normalizeStaticSuggestions(config.suggestions); built = { ...config, suggestions: normalizedSuggestions }; } const serialized = JSON.stringify(built); const cache = normalizationCacheRef.current; if (cache.serialized === serialized && cache.config) return { normalizedConfig: cache.config, serializedConfig: serialized }; normalizationCacheRef.current = { serialized, config: built }; return { normalizedConfig: built, serializedConfig: serialized }; }, [ config, resolvedConsumerAgentId, ...extraDeps ]); const latestConfigRef = useRef(null); latestConfigRef.current = normalizedConfig; const previousSerializedConfigRef = useRef(null); const targetAgentId = useMemo(() => { if (!normalizedConfig) return resolvedConsumerAgentId; const consumer = normalizedConfig.consumerAgentId; if (!consumer || consumer === "*") return resolvedConsumerAgentId; return consumer; }, [normalizedConfig, resolvedConsumerAgentId]); const isGlobalConfig = rawConsumerAgentId === void 0 || rawConsumerAgentId === "*"; const isDynamicConfigType = useMemo(() => !!normalizedConfig && "instructions" in normalizedConfig, [normalizedConfig]); const requestReload = useCallback(() => { if (!normalizedConfig) return; if (isGlobalConfig) { const seen = /* @__PURE__ */ new Set(); const agents = Object.values(copilotkit.agents ?? {}); for (const entry of agents) { const agentId = entry.agentId; if (!agentId) continue; seen.add(agentId); if (!entry.isRunning) copilotkit.reloadSuggestions(agentId); } if (targetAgentId && !seen.has(targetAgentId)) copilotkit.reloadSuggestions(targetAgentId); return; } if (!targetAgentId) return; copilotkit.reloadSuggestions(targetAgentId); }, [ copilotkit, isGlobalConfig, normalizedConfig, targetAgentId ]); useEffect(() => { if (!serializedConfig || !latestConfigRef.current) return; const id = copilotkit.addSuggestionsConfig(latestConfigRef.current); requestReload(); return () => { copilotkit.removeSuggestionsConfig(id); }; }, [ copilotkit, serializedConfig, requestReload ]); useEffect(() => { if (!normalizedConfig) { previousSerializedConfigRef.current = null; return; } if (serializedConfig && previousSerializedConfigRef.current === serializedConfig) return; if (serializedConfig) previousSerializedConfigRef.current = serializedConfig; requestReload(); }, [ normalizedConfig, requestReload, serializedConfig ]); useEffect(() => { if (!normalizedConfig || extraDeps.length === 0) return; requestReload(); }, [ extraDeps.length, normalizedConfig, requestReload, ...extraDeps ]); useEffect(() => { if (!normalizedConfig || !isDynamicConfigType) return; if (!targetAgentId) return; if (!!copilotkit.getAgent(targetAgentId)) return; const subscription = copilotkit.subscribe({ onAgentsChanged: () => { if (copilotkit.getAgent(targetAgentId)) { requestReload(); subscription.unsubscribe(); } } }); return () => { subscription.unsubscribe(); }; }, [ copilotkit, normalizedConfig, isDynamicConfigType, targetAgentId, requestReload ]); } function isDynamicConfig(config) { return "instructions" in config; } function normalizeStaticSuggestions(suggestions) { return suggestions.map((suggestion) => ({ ...suggestion, isLoading: suggestion.isLoading ?? false })); } //#endregion //#region src/v2/hooks/use-agent-context.tsx function useAgentContext(context) { const { description, value } = context; const { copilotkit } = useCopilotKit(); const stringValue = useMemo(() => { if (typeof value === "string") return value; return JSON.stringify(value); }, [value]); useLayoutEffect(() => { if (!copilotkit) return; const id = copilotkit.addContext({ description, value: stringValue }); return () => { copilotkit.removeContext(id); }; }, [ description, stringValue, copilotkit ]); } //#endregion //#region src/v2/hooks/use-threads.tsx function useThreadStoreSelector(store, selector) { return useSyncExternalStore(useCallback((onStoreChange) => { const subscription = store.select(selector).subscribe(onStoreChange); return () => subscription.unsubscribe(); }, [store, selector]), () => selector(store.getState())); } /** * React hook for listing and managing Intelligence platform threads. * * On mount the hook fetches the thread list for the runtime-authenticated user * and the given `agentId`. When the Intelligence platform exposes a WebSocket * URL, it also opens a realtime subscription so the `threads` array stays * current without polling — thread creates, renames, archives, and deletes * from any client are reflected immediately. * * Mutation methods (`renameThread`, `archiveThread`, `deleteThread`) return * promises that resolve once the platform confirms the operation and reject * with an `Error` on failure. * * @param input - Agent identifier and optional list controls. * @returns Thread list state and stable mutation callbacks. * * @example * ```tsx * import { useThreads } from "@copilotkit/react-core"; * * function ThreadList() { * const { threads, isLoading, renameThread, deleteThread } = useThreads({ * agentId: "agent-1", * }); * * if (isLoading) return <p>Loading…</p>; * * return ( * <ul> * {threads.map((t) => ( * <li key={t.id}> * {t.name ?? "Untitled"} * <button onClick={() => renameThread(t.id, "New name")}>Rename</button> * <button onClick={() => deleteThread(t.id)}>Delete</button> * </li> * ))} * </ul> * ); * } * ``` */ function useThreads({ agentId, includeArchived, limit }) { const { copilotkit } = useCopilotKit(); const [store] = useState(() => ɵcreateThreadStore({ fetch: globalThis.fetch })); const coreThreads = useThreadStoreSelector(store, ɵselectThreads); const threads = useMemo(() => coreThreads.map(({ id, agentId, name, archived, createdAt, updatedAt, lastRunAt }) => ({ id, agentId, name, archived, createdAt, updatedAt, ...lastRunAt !== void 0 ? { lastRunAt } : {} })), [coreThreads]); const storeIsLoading = useThreadStoreSelector(store, ɵselectThreadsIsLoading); const storeError = useThreadStoreSelector(store, ɵselectThreadsError); const hasMoreThreads = useThreadStoreSelector(store, ɵselectHasNextPage); const isFetchingMoreThreads = useThreadStoreSelector(store, ɵselectIsFetchingNextPage); const headersKey = useMemo(() => { return JSON.stringify(Object.entries(copilotkit.headers ?? {}).sort(([left], [right]) => left.localeCompare(right))); }, [copilotkit.headers]); const runtimeError = useMemo(() => { if (copilotkit.runtimeUrl) return null; return /* @__PURE__ */ new Error("Runtime URL is not configured"); }, [copilotkit.runtimeUrl]); const [hasDispatchedContext, setHasDispatchedContext] = useState(false); const preConnectLoading = !!copilotkit.runtimeUrl && !hasDispatchedContext; const isLoading = runtimeError ? false : preConnectLoading || storeIsLoading; const error = runtimeError ?? storeError; useEffect(() => { store.start(); return () => { store.stop(); }; }, [store]); const runtimeStatus = copilotkit.runtimeConnectionStatus; useEffect(() => { copilotkit.registerThreadStore(agentId, store); return () => { copilotkit.unregisterThreadStore(agentId); }; }, [ copilotkit, agentId, store ]); useEffect(() => { if (!copilotkit.runtimeUrl) { store.setContext(null); return; } if (runtimeStatus !== CopilotKitCoreRuntimeConnectionStatus.Connected) return; const context = { runtimeUrl: copilotkit.runtimeUrl, headers: { ...copilotkit.headers }, wsUrl: copilotkit.intelligence?.wsUrl, agentId, includeArchived, limit }; store.setContext(context); setHasDispatchedContext(true); }, [ store, copilotkit.runtimeUrl, runtimeStatus, headersKey, copilotkit.intelligence?.wsUrl, agentId, includeArchived, limit ]); const renameThread = useCallback((threadId, name) => store.renameThread(threadId, name), [store]); const archiveThread = useCallback((threadId) => store.archiveThread(threadId), [store]); const deleteThread = useCallback((threadId) => store.deleteThread(threadId), [store]); return { threads, isLoading, error, hasMoreThreads, isFetchingMoreThreads, fetchMoreThreads: useCallback(() => store.fetchNextPage(), [store]), renameThread, archiveThread, deleteThread }; } //#endregion //#region src/v2/types/defineToolCallRenderer.ts function defineToolCallRenderer(def) { const argsSchema = def.name === "*" && !def.args ? z.any() : def.args; return { name: def.name, args: argsSchema, render: def.render, ...def.agentId ? { agentId: def.agentId } : {} }; } //#endregion //#region src/v2/hooks/use-render-tool.tsx const EMPTY_DEPS = []; /** * Registers a renderer entry in CopilotKit's `renderToolCalls` registry. * * Key behavior: * - deduplicates by `agentId:name` (latest registration wins), * - keeps renderer entries on cleanup so historical chat tool calls can still render, * - refreshes registration when `deps` change. * * @typeParam S - Schema type describing tool call parameters. * @param config - Renderer config for wildcard or named tools. * @param deps - Optional dependencies to refresh registration. * * @example * ```tsx * useRenderTool( * { * name: "searchDocs", * parameters: z.object({ query: z.string() }), * render: ({ status, parameters, result }) => { * if (status === "executing") return <div>Searching {parameters.query}</div>; * if (status === "complete") return <div>{result}</div>; * return <div>Preparing...</div>; * }, * }, * [], * ); * ``` * * @example * ```tsx * useRenderTool( * { * name: "summarize", * parameters: z.object({ text: z.string() }), * agentId: "research-agent", * render: ({ name, status }) => <div>{name}: {status}</div>, * }, * [selectedAgentId], * ); * ``` */ function useRenderTool(config, deps) { const { copilotkit } = useCopilotKit(); const extraDeps = deps ?? EMPTY_DEPS; useEffect(() => { const renderer = config.name === "*" && !config.parameters ? defineToolCallRenderer({ name: "*", render: (props) => config.render({ ...props, parameters: props.args }), ...config.agentId ? { agentId: config.agentId } : {} }) : defineToolCallRenderer({ name: config.name, args: config.parameters, render: (props) => config.render({ ...props, parameters: props.args }), ...config.agentId ? { agentId: config.agentId } : {} }); copilotkit.addHookRenderToolCall(renderer); }, [ config.name, copilotkit, JSON.stringify(extraDeps) ]); } //#endregion //#region src/v2/hooks/use-capabilities.tsx /** * Returns the capabilities declared by the given agent (or the default agent). * Capabilities are populated from the runtime `/info` response at connection * time. The hook reads them synchronously from the agent instance — there is * no separate loading state, but the value will be `undefined` until the * runtime handshake completes. * * @param agentId - Optional agent ID. If omitted, uses the default agent. * @returns The agent's capabilities, or `undefined` if the agent doesn't * declare capabilities. */ function useCapabilities(agentId) { const { agent } = useAgent({ agentId }); if (agent && "capabilities" in agent) return agent.capabilities; } //#endregion export { CopilotChatConfigurationProvider, CopilotChatDefaultLabels, CopilotKitCoreReact, defineToolCallRenderer, useAgent, useAgentContext, useCapabilities, useComponent, useConfigureSuggestions, useCopilotChatConfiguration, useFrontendTool, useHumanInTheLoop, useInterrupt, useRenderTool, useSuggestions, useThreads }; //# sourceMappingURL=headless.mjs.map