/** * * Usage of this hook assumes some additional setup in your application, for more information * on that see the CoAgents [getting started guide](/coagents/quickstart/langgraph). * * * CoAgents demonstration * * * 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 { useAiAgent } from "@vn-sdk/react-core"; * * type AgentState = { * count: number; * } * * const agent = useAiAgent({ * name: "my-agent", * initialState: { * count: 0, * }, * }); * * ``` * * `useAiAgent` 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 } = useAiAgent({ * name: "my-agent", * initialState: { * count: 0, * }, * }); * * return ( *
*

Count: {state.count}

* *
* ); * ``` * * This reactivity is bidirectional, meaning that changes to the state from the agent will be reflected in the UI and vice versa. * * ## Parameters * * The options to use when creating the coagent. * * The name of the agent to use. * * * The initial state of the agent. * * * State to manage externally if you are using this hook with external state management. * * * A function to update the state of the agent if you are using this hook with external state management. * * */ import { useCallback, useEffect, useMemo, useRef } from "react"; import { AiContextParams, useAiContext } from "../context"; import { AiAgentState } from "../types/ai-agent-state"; import { useAiChat } from "./use-ai-chat-internal"; import { Message } from "@vn-sdk/shared"; import { useAsyncCallback } from "../components/error-boundary/error-utils"; import { useToast } from "../components/toast/toast-provider"; import { useAiRuntimeClient } from "./use-ai-runtime-client"; import { parseJson, AiSDKAgentDiscoveryError } from "@vn-sdk/shared"; import { useMessagesTap } from "../components/ai-provider/ai-messages"; import { Message as GqlMessage } from "@vn-sdk/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; /** * Config to pass to a LangGraph Agent */ config?: { configurable?: Record; [key: string]: any; }; } interface WithInternalStateManagementAndInitial extends UseCoagentOptionsBase { /** * The initial state of the agent. */ initialState: T; } interface WithInternalStateManagement extends UseCoagentOptionsBase { /** * Optional initialState with default type any */ initialState?: any; } interface WithExternalStateManagement 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 = | WithInternalStateManagementAndInitial | WithInternalStateManagement | WithExternalStateManagement; export interface UseCoagentReturnType { /** * 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; } 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.vn.ai/coagents/quickstart/langgraph. */ export function useAiAgent(options: UseCoagentOptions): UseCoagentReturnType { const context = useAiContext(); const { availableAgents, onError } = context; const { setBannerError } = useToast(); const lastLoadedThreadId = useRef(); const lastLoadedState = useRef(); const { name } = options; useEffect(() => { if (availableAgents?.length && !availableAgents.some((a) => a.name === name)) { const message = `(useAiAgent): 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 AiSDKAgentDiscoveryError({ agentName: name, availableAgents: availableAgents.map((a) => ({ name: a.name, id: a.id })), }); setBannerError(agentError); } }, [availableAgents]); const { getMessagesFromTap } = useMessagesTap(); const { aiAgentStates, aiAgentStatesRef, setAiAgentStatesWithRef, threadId, aiApiConfig } = context; const { sendMessage, runChatCompletion } = useAiChat(); const headers = { ...(aiApiConfig.headers || {}), }; const runtimeClient = useAiRuntimeClient({ url: aiApiConfig.chatApiEndpoint, publicApiKey: aiApiConfig.publicApiKey, headers, credentials: aiApiConfig.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)) => { // aiAgentStatesRef.current || {} let coagentState: AiAgentState = getAiAgentState({ aiAgentStates, name, options }); const updatedState = typeof newState === "function" ? (newState as Function)(coagentState.state) : newState; setAiAgentStatesWithRef({ ...aiAgentStatesRef.current, [name]: { ...coagentState, state: updatedState, }, }); }, [aiAgentStates, 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 (aiAgentStates[name] === undefined) { setState(options.initialState === undefined ? {} : options.initialState); } }, [ isExternalStateManagement(options) ? JSON.stringify(options.state) : undefined, // reset initialstate on reset aiAgentStates[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; setAiAgentStatesWithRef((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 = getAiAgentState({ aiAgentStates, 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, aiAgentStates, options, setState, runAgentCallback]); } export function startAgent(name: string, context: AiContextParams) { const { setAgentSession } = context; setAgentSession({ agentName: name, }); } export function stopAgent(name: string, context: AiContextParams) { const { agentSession, setAgentSession } = context; if (agentSession && agentSession.agentName === name) { setAgentSession(null); context.setAiAgentStates((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: AiContextParams, messages: GqlMessage[], sendMessage: (message: Message) => Promise, runChatCompletion: () => Promise, 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.aiAgentStatesRef.current?.[name]?.state || {}; if (hint) { const hintMessage = hint({ previousState, currentState: state }); if (hintMessage) { await sendMessage(hintMessage); } else { await runChatCompletion(); } } else { await runChatCompletion(); } } const isExternalStateManagement = ( options: UseCoagentOptions, ): options is WithExternalStateManagement => { return "state" in options && "setState" in options; }; const isInternalStateManagementWithInitial = ( options: UseCoagentOptions, ): options is WithInternalStateManagementAndInitial => { return "initialState" in options; }; const getAiAgentState = ({ aiAgentStates, name, options, }: { aiAgentStates: Record; name: string; options: UseCoagentOptions; }) => { if (aiAgentStates[name]) { return aiAgentStates[name]; } else { return { name, state: isInternalStateManagementWithInitial(options) ? options.initialState : {}, config: options.config ? options.config : options.configurable ? { configurable: options.configurable } : {}, running: false, active: false, threadId: undefined, nodeName: undefined, runId: undefined, }; } };