@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;" />
478 lines (438 loc) • 14.5 kB
text/typescript
/**
* <Callout type="info">
* Usage of this hook assumes some additional setup in your application, for more information
* on that see the CoAgents <span className="text-blue-500">[getting started guide](/coagents/quickstart/langgraph)</span>.
* </Callout>
* <Frame className="my-12">
* <img
* src="https://cdn.copilotkit.ai/docs/copilotkit/images/coagents/SharedStateCoAgents.gif"
* alt="CoAgents demonstration"
* className="w-auto"
* />
* </Frame>
*
* This hook is used to integrate an agent into your application. With its use, you can
* render and update the state of an agent, allowing for a dynamic and interactive experience.
* We call these shared state experiences agentic copilots, or CoAgents for short.
*
* ## Usage
*
* ### Simple Usage
*
* ```tsx
* import { useCoAgent } from "@copilotkit/react-core";
*
* type AgentState = {
* count: number;
* }
*
* const agent = useCoAgent<AgentState>({
* name: "my-agent",
* initialState: {
* count: 0,
* },
* });
*
* ```
*
* `useCoAgent` returns an object with the following properties:
*
* ```tsx
* const {
* name, // The name of the agent currently being used.
* nodeName, // The name of the current LangGraph node.
* state, // The current state of the agent.
* setState, // A function to update the state of the agent.
* running, // A boolean indicating if the agent is currently running.
* start, // A function to start the agent.
* stop, // A function to stop the agent.
* run, // A function to re-run the agent. Takes a HintFunction to inform the agent why it is being re-run.
* } = agent;
* ```
*
* Finally we can leverage these properties to create reactive experiences with the agent!
*
* ```tsx
* const { state, setState } = useCoAgent<AgentState>({
* name: "my-agent",
* initialState: {
* count: 0,
* },
* });
*
* return (
* <div>
* <p>Count: {state.count}</p>
* <button onClick={() => setState({ count: state.count + 1 })}>Increment</button>
* </div>
* );
* ```
*
* This reactivity is bidirectional, meaning that changes to the state from the agent will be reflected in the UI and vice versa.
*
* ## Parameters
* <PropertyReference name="options" type="UseCoagentOptions<T>" required>
* The options to use when creating the coagent.
* <PropertyReference name="name" type="string" required>
* The name of the agent to use.
* </PropertyReference>
* <PropertyReference name="initialState" type="T | any">
* The initial state of the agent.
* </PropertyReference>
* <PropertyReference name="state" type="T | any">
* State to manage externally if you are using this hook with external state management.
* </PropertyReference>
* <PropertyReference name="setState" type="(newState: T | ((prevState: T | undefined) => T)) => void">
* A function to update the state of the agent if you are using this hook with external state management.
* </PropertyReference>
* </PropertyReference>
*/
import { useCallback, useEffect, useMemo, useRef } from "react";
import { CopilotContextParams, useCopilotContext } from "../context";
import { CoagentState } from "../types/coagent-state";
import { useCopilotChat } from "./use-copilot-chat_internal";
import { Message } from "@copilotkit/shared";
import { useAsyncCallback } from "../components/error-boundary/error-utils";
import { useToast } from "../components/toast/toast-provider";
import { useCopilotRuntimeClient } from "./use-copilot-runtime-client";
import { parseJson, CopilotKitAgentDiscoveryError } from "@copilotkit/shared";
import { useMessagesTap } from "../components/copilot-provider/copilot-messages";
import { Message as GqlMessage } from "@copilotkit/runtime-client-gql";
interface UseCoagentOptionsBase {
/**
* The name of the agent being used.
*/
name: string;
/**
* @deprecated - use "config.configurable"
* Config to pass to a LangGraph Agent
*/
configurable?: Record<string, any>;
/**
* Config to pass to a LangGraph Agent
*/
config?: {
configurable?: Record<string, any>;
[key: string]: any;
};
}
interface WithInternalStateManagementAndInitial<T> extends UseCoagentOptionsBase {
/**
* The initial state of the agent.
*/
initialState: T;
}
interface WithInternalStateManagement extends UseCoagentOptionsBase {
/**
* Optional initialState with default type any
*/
initialState?: any;
}
interface WithExternalStateManagement<T> extends UseCoagentOptionsBase {
/**
* The current state of the agent.
*/
state: T;
/**
* A function to update the state of the agent.
*/
setState: (newState: T | ((prevState: T | undefined) => T)) => void;
}
type UseCoagentOptions<T> =
| WithInternalStateManagementAndInitial<T>
| WithInternalStateManagement
| WithExternalStateManagement<T>;
export interface UseCoagentReturnType<T> {
/**
* The name of the agent being used.
*/
name: string;
/**
* The name of the current LangGraph node.
*/
nodeName?: string;
/**
* The ID of the thread the agent is running in.
*/
threadId?: string;
/**
* A boolean indicating if the agent is currently running.
*/
running: boolean;
/**
* The current state of the agent.
*/
state: T;
/**
* A function to update the state of the agent.
*/
setState: (newState: T | ((prevState: T | undefined) => T)) => void;
/**
* A function to start the agent.
*/
start: () => void;
/**
* A function to stop the agent.
*/
stop: () => void;
/**
* A function to re-run the agent. The hint function can be used to provide a hint to the agent
* about why it is being re-run again.
*/
run: (hint?: HintFunction) => Promise<void>;
}
export interface HintFunctionParams {
/**
* The previous state of the agent.
*/
previousState: any;
/**
* The current state of the agent.
*/
currentState: any;
}
export type HintFunction = (params: HintFunctionParams) => Message | undefined;
/**
* This hook is used to integrate an agent into your application. With its use, you can
* render and update the state of the agent, allowing for a dynamic and interactive experience.
* We call these shared state experiences "agentic copilots". To get started using agentic copilots, which
* we refer to as CoAgents, checkout the documentation at https://docs.copilotkit.ai/coagents/quickstart/langgraph.
*/
export function useCoAgent<T = any>(options: UseCoagentOptions<T>): UseCoagentReturnType<T> {
const context = useCopilotContext();
const { availableAgents, onError } = context;
const { setBannerError } = useToast();
const lastLoadedThreadId = useRef<string>();
const lastLoadedState = useRef<any>();
const { name } = options;
useEffect(() => {
if (availableAgents?.length && !availableAgents.some((a) => a.name === name)) {
const message = `(useCoAgent): Agent "${name}" not found. Make sure the agent exists and is properly configured.`;
console.warn(message);
// Route to banner instead of toast for consistency
const agentError = new CopilotKitAgentDiscoveryError({
agentName: name,
availableAgents: availableAgents.map((a) => ({ name: a.name, id: a.id })),
});
setBannerError(agentError);
}
}, [availableAgents]);
const { getMessagesFromTap } = useMessagesTap();
const { coagentStates, coagentStatesRef, setCoagentStatesWithRef, threadId, copilotApiConfig } =
context;
const { sendMessage, runChatCompletion } = useCopilotChat();
const headers = {
...(copilotApiConfig.headers || {}),
};
const runtimeClient = useCopilotRuntimeClient({
url: copilotApiConfig.chatApiEndpoint,
publicApiKey: copilotApiConfig.publicApiKey,
headers,
credentials: copilotApiConfig.credentials,
showDevConsole: context.showDevConsole,
onError,
});
// if we manage state internally, we need to provide a function to set the state
const setState = useCallback(
(newState: T | ((prevState: T | undefined) => T)) => {
// coagentStatesRef.current || {}
let coagentState: CoagentState = getCoagentState({ coagentStates, name, options });
const updatedState =
typeof newState === "function" ? (newState as Function)(coagentState.state) : newState;
setCoagentStatesWithRef({
...coagentStatesRef.current,
[name]: {
...coagentState,
state: updatedState,
},
});
},
[coagentStates, name],
);
useEffect(() => {
const fetchAgentState = async () => {
if (!threadId || threadId === lastLoadedThreadId.current) return;
const result = await runtimeClient.loadAgentState({
threadId,
agentName: name,
});
// Runtime client handles errors automatically via handleGQLErrors
if (result.error) {
return; // Don't process data on error
}
const newState = result.data?.loadAgentState?.state;
if (newState === lastLoadedState.current) return;
if (result.data?.loadAgentState?.threadExists && newState && newState != "{}") {
lastLoadedState.current = newState;
lastLoadedThreadId.current = threadId;
const fetchedState = parseJson(newState, {});
isExternalStateManagement(options)
? options.setState(fetchedState)
: setState(fetchedState);
}
};
void fetchAgentState();
}, [threadId]);
// Sync internal state with external state if state management is external
useEffect(() => {
if (isExternalStateManagement(options)) {
setState(options.state);
} else if (coagentStates[name] === undefined) {
setState(options.initialState === undefined ? {} : options.initialState);
}
}, [
isExternalStateManagement(options) ? JSON.stringify(options.state) : undefined,
// reset initialstate on reset
coagentStates[name] === undefined,
]);
// Sync config when runtime configuration changes
useEffect(() => {
const newConfig = options.config
? options.config
: options.configurable
? { configurable: options.configurable }
: undefined;
if (newConfig === undefined) return;
setCoagentStatesWithRef((prev) => {
const existing = prev[name] ?? {
name,
state: isInternalStateManagementWithInitial(options) ? options.initialState : {},
config: {},
running: false,
active: false,
threadId: undefined,
nodeName: undefined,
runId: undefined,
};
if (JSON.stringify(existing.config) === JSON.stringify(newConfig)) {
return prev;
}
return {
...prev,
[name]: {
...existing,
config: newConfig,
},
};
});
}, [JSON.stringify(options.config), JSON.stringify(options.configurable)]);
const runAgentCallback = useAsyncCallback(
async (hint?: HintFunction) => {
await runAgent(name, context, getMessagesFromTap(), sendMessage, runChatCompletion, hint);
},
[name, context, sendMessage, runChatCompletion],
);
// Return the state and setState function
return useMemo(() => {
const coagentState = getCoagentState({ coagentStates, name, options });
return {
name,
nodeName: coagentState.nodeName,
threadId: coagentState.threadId,
running: coagentState.running,
state: coagentState.state,
setState: isExternalStateManagement(options) ? options.setState : setState,
start: () => startAgent(name, context),
stop: () => stopAgent(name, context),
run: runAgentCallback,
};
}, [name, coagentStates, options, setState, runAgentCallback]);
}
export function startAgent(name: string, context: CopilotContextParams) {
const { setAgentSession } = context;
setAgentSession({
agentName: name,
});
}
export function stopAgent(name: string, context: CopilotContextParams) {
const { agentSession, setAgentSession } = context;
if (agentSession && agentSession.agentName === name) {
setAgentSession(null);
context.setCoagentStates((prevAgentStates) => {
return {
...prevAgentStates,
[name]: {
...prevAgentStates[name],
running: false,
active: false,
threadId: undefined,
nodeName: undefined,
runId: undefined,
},
};
});
} else {
console.warn(`No agent session found for ${name}`);
}
}
export async function runAgent(
name: string,
context: CopilotContextParams,
messages: GqlMessage[],
sendMessage: (message: Message) => Promise<void>,
runChatCompletion: () => Promise<Message[]>,
hint?: HintFunction,
) {
const { agentSession, setAgentSession } = context;
if (!agentSession || agentSession.agentName !== name) {
setAgentSession({
agentName: name,
});
}
let previousState: any = null;
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (message.isAgentStateMessage() && message.agentName === name) {
previousState = message.state;
}
}
let state = context.coagentStatesRef.current?.[name]?.state || {};
if (hint) {
const hintMessage = hint({ previousState, currentState: state });
if (hintMessage) {
await sendMessage(hintMessage);
} else {
await runChatCompletion();
}
} else {
await runChatCompletion();
}
}
const isExternalStateManagement = <T>(
options: UseCoagentOptions<T>,
): options is WithExternalStateManagement<T> => {
return "state" in options && "setState" in options;
};
const isInternalStateManagementWithInitial = <T>(
options: UseCoagentOptions<T>,
): options is WithInternalStateManagementAndInitial<T> => {
return "initialState" in options;
};
const getCoagentState = <T>({
coagentStates,
name,
options,
}: {
coagentStates: Record<string, CoagentState>;
name: string;
options: UseCoagentOptions<T>;
}) => {
if (coagentStates[name]) {
return coagentStates[name];
} else {
return {
name,
state: isInternalStateManagementWithInitial<T>(options) ? options.initialState : {},
config: options.config
? options.config
: options.configurable
? { configurable: options.configurable }
: {},
running: false,
active: false,
threadId: undefined,
nodeName: undefined,
runId: undefined,
};
}
};