#onError in <CopilotKit> not triggering

1 messages · Page 1 of 1 (latest)

scarlet marsh
#

When I have a problem with my backend (eg. server is offline), I get this console error:

AppLayoutClient.tsx:43 CopilotKit Error (hidden in production): Agent 'sample_agent' was not found. Please set up at least one agent before proceeding. See more: [https://docs.copilotkit.ai/coagents/troubleshooting/common-issues#i-am-gett…

To be able to handle this and show an error message to the user, I've added this to the code:

    <CopilotKit
      publicApiKey={copilotkitPublicApiKey}
      agent="sample_agent"
      onError={(error: unknown) => {
        toast.error(
          error instanceof Error
            ? error.message
            : 'Could not connect to AI assistant',
        );
      }}
    >

But the onError function is not being called. I still see the error message on the console, but any code I add to the onError function (including a simple console.log for sanity check) doesn't get executed.

magic otterBOT
#

The error handling logic requires both onError and publicApiKey to be defined. If publicApiKey is missing, invalid, or there are connection issues, the onError handler won't be triggered . This is a known limitation in the current implementation.

You can use a try catch wrapper or self hosted runtime, if this is a priority.

Here is a [related discussion](#1390077651187662938 message)

on this topic. Hope this helps.

frank hound
#

Hi @hexed bough , I am running into this same issue. We are using a self hosted runtime and passing a publicApiKey. When testing the onError handler via Playwright and mocking the response to status 500, I am still seeing that onError never gets called.

And actually, it seems that there is some default internal error handling that pops up a toast message. We need a way to opt out of this default error handling and have onError trigger as one might expect here.

Any suggestions?

magic otterBOT
#

Can you please try out with passing just the publicApiKey and not use the runtime?. Please check and tell if that resolves the issue?

fresh shoal
#

Hi I'm on Paul's team. The response is mocked - the runtime would not be touching the request at all (at least that's my understanding). So this already is just an onError cb for the CopilotKit provider component that is set up as described in the docs.

frank hound
#

+1, as I mentioned I am validating this via Playwright testing and mocking the response to explicitly return status 500 (or any 4xx/5xx status). So this behavior appears to be consistently reproducible when any endpoint has a network error.

magic otterBOT
#

Just to confirm first -  are you currently passing a public API key or a public license key?
As per this doc, 'Error observability hooks will not trigger without a valid publicLicenseKey. The onError hook is specifically for error observability'

Monitor your CopilotKit application with comprehensive observability hooks. Understand user interactions, chat events, and system errors.

fresh shoal
#

Oh we do not have that publicLicenseKey. Your message on 7/25 mentions using a 'publicApiKey', so we went off of that. With regards to the onError hook - is this just meant for error observability purposes? If a user faces issues with connecting to their LLM backend during tool execution, and wants to display some kind of message when they get a 503 from their LLM, is there some other way to observe that? As of now, the default error experience (an empty chat) is not very user friendly IMO.

hexed bough
#

Yeah, it was a typo, you can get a publicLicenseKey from dashboard.
The onError hook provides detailed, structured events you can forward to your monitoring stack.

Refer here

Learn how to debug errors in CopilotKit with dev console and set up error observability for monitoring services.

frank hound
#

We simply want to be able to handle generic network errors if/when our own runtime URL returns a non-200 status code. Currently, it seems there is no way to handle this, since onError never triggers and there is no way to opt out of the default Toast error message I shared in the screenshot on this original post.

We are not asking about monitoring, but how to gracefully handle errors in the front end. It seems that custom error handling is not exposed by the react-core library, and there is no way to opt out of the default toast message behavior imposed by the library.

hexed bough
#

Did you try onerror with a public licenseKey?

frank hound
#

Does it need to be a valid publicLicenseKey? We don't want to make external API calls just to be able to catch and respond to an error from our own service.

hexed bough
#

Currently onError will trigger only with a public licenseKey

frank hound
#

Heard. But not sure I understand. Are you saying we need to call an external service just to be able to catch generic errors from our own server's response? Or just that the front end expects some dummy value there in order for onError to be triggered.

#

Sounds like we have to fork or roll up our own UI for this, if there is no way to customize how the library handles network errors from our own runtime....

Not sure why we'd need a public licenseKey to do something this straightforward. But please let me know if I'm misunderstanding anything.

forest stag
#

@hexed bough Why does a api key need to be provided for an onError callback to our own service? Can an update be made to remove this requirement? Is there an alternative way we can handle copilotkit runtime errors from the client?

hexed bough
hexed bough
hexed bough
wintry olive
#

Thanks for looking into this. I think this is still happening, at least I'm seeing this with 1.50.2.

magic otterBOT
#

Hi @wintry olive, could you share a screenshot of what you're seeing and some details about your setup? That'll help us figure out what's going on. Thanks!

wintry olive
#

I've tried adding onError to the CopilotKit tag and it's never called on RUN_ERROR (nor the network errors where it couldn't connect). I added this on my backend to programatically return one, and the UI just stops processing and there's no error. If I turn on showDevConsole={true} then I'll get the toast at the bottom, but with devconsole off, nothing there. I put in the public license and as an api key since that's needed for error handling to work, neither fixed it (and I confirmed in the debugger that the apikey was set)


                        error_event = RunErrorEvent(
                            type=EventType.RUN_ERROR,
                            message="Event encoding failed: Boo!",
                            code="ENCODING_ERROR",
                        )
                        yield encoder.encode(error_event)
#

I've tried this:

export function CopilotProvider({ children }: CopilotProviderProps) {
  console.log("License: ", process.env.NEXT_PUBLIC_COPILOT_LICENSE_KEY)
  return (
    <CopilotKit
      runtimeUrl="/api/copilotkit"
      agent={COPILOT_AGENT_NAME}
      publicLicenseKey={process.env.NEXT_PUBLIC_COPILOT_LICENSE_KEY}
      publicApiKey={process.env.NEXT_PUBLIC_COPILOT_LICENSE_KEY}
      showDevConsole={false}
      enableInspector={false}
      onError={handleCopilotError}
    >
      {children}
    {/* <CopilotErrorToast /> */}
     <EventLogger />
    </CopilotKit>
  )
}

also on the sidebar component:

      <div className={cn(isChatFullScreen && "copilot-sidebar-fullscreen")}>
        <CopilotSidebar
          Header={ChatHeader}
          Input={SlashCommandInput}
          labels={{
            title: "Apprentice",
            initial: chatPreferences.greeting,
          }}
          shortcut="\\"
          onThumbsUp={handleCopilotThumbsUp}
          onThumbsDown={handleCopilotThumbsDown}
          onCopy={handleChatCopy}
          onError={(error) => {
            console.error("Copilot Sidebar error:", error)
          }}
          clickOutsideToClose={false}
          className={isChatFullScreen ? "" : "mb-10 -mr-1"}
          suggestions={[
            { title: "Show all insights", message: "Show all insights" },
            { title: "High priority insights", message: "Show insights with priority above 70" },
            { title: "Show all projects", message: "Show all projects" },
          ]}
        />
      </div>
#

I've tried adding it through the useAgent() hook using the EventLogger function I made:


export function EventLogger() {
  const { agent } = useAgent({agentId: COPILOT_AGENT_NAME});
  useEffect(() => {
    console.log("EventLogger mounted");
    const subscriber: AgentSubscriber = {
      onRunInitialized: (params) => {
        console.log("Agent run initialized", params);
      },
      onEvent(event) {
        console.log("Agent event:", event);
      },
      onCustomEvent: ({ event }) => {
        console.log("Custom event:", event.name, event.value);
      },
      onRunStartedEvent: () => {
        console.log("Agent started running");
      },
      onRunFinalized: () => {
        console.log("Agent finished running");
      },
      onStateChanged: (state) => {
        console.log("State changed:", state);
      },
      onRunErrorEvent(params) {
        console.error("Agent run error:", params);
      },
    };
    const { unsubscribe } = agent.subscribe(subscriber);
    return () => {
      unsubscribe()
      };
  }, []);
  return null;
}
#

Thanks Tony!! Appreciate any advice or ideas you have!

magic otterBOT
#

Based on my understanding, it should work if the agent IDs match and the backend emits run_id and agent_id. Then onRunErrorEvent should fire and you can handle it programmatically.
Let me verify this with the team to make sure nothing's off. Thanks!

wintry olive
#

Thanks, the agent_id and agent (name?) are the same (since the Copilotkit function just has agent= for it's parameter) and matches what I use in useCoAgent's name parameter. So it should match. Also if I turn on devConsole={true} in Copilotkit, then I'll see it's toast appear.

rigid moon
#

@wintry olive, are you also providing the agent name at runtime?
LangGraph example -

const runtime = new CopilotRuntime({
  agents: {
    sample_agent: new LangGraphAgent({
      deploymentUrl:
        process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123",
      graphId: "sample_agent",
      langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
    }),
  },
});
#

Are you using an agent framework?

wintry olive
#

On the backend I'm using google adk with the ag-ui middleware. Here's the TS code for that (it's a bit more complex, since I'm using cloud run and have to inject auth headers)

#
// Get GCP configuration from environment
const gcpConfig = getGCPConfigFromEnv();

// Initialize the External Account Client
const authClient = createExternalAccountClient(gcpConfig);

// Create singleton agent and runtime (as per CopilotKit v1.50 examples)
const agent: AbstractAgent = new AuthenticatedHttpAgent({ url: AGENT_URL });

/**
 * Agent configuration for CopilotRuntime.
 *
 * CopilotKit v1.51+ has complex type constraints between @copilotkit/runtime
 * and @copilotkitnext/runtime. We use a type assertion to work around this.
 * The 'default' key ensures the config is never empty.
 */
const agentsConfig = {
    [COPILOT_AGENT_NAME]: agent,
    default: agent,  // Ensures config is never empty + provides fallback
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const runtime = new CopilotRuntime({ agents: agentsConfig as any });

/**
 * ExperimentalEmptyAdapter is a no-op LLM adapter for AG-UI mode.
 * When using AG-UI protocol, the agent handles all LLM communication,
 * so CopilotKit doesn't need a direct LLM connection.
 * @see https://docs.copilotkit.ai/coagents/agentic-copilot
 */
const serviceAdapter = new ExperimentalEmptyAdapter();

// Create the handler once (singleton pattern)
const handler = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter,
    endpoint: "/api/copilotkit",
});

#

and "COPILOT_AGENT_NAME" is a constant I defined since there were bunches of places that all wanted an agent id or agent name (and it's simply "my-agent")

#
class AuthenticatedHttpAgent extends HttpAgent {
    private static requestHeaders: Map<string, { headers: Record<string, string>; timestamp: number }> = new Map();
    private static cleanupInterval: ReturnType<typeof setInterval> | null = null;

    constructor(config: { url: string }) {
        super(config);
        // Start cleanup interval if not already running
        AuthenticatedHttpAgent.startCleanupInterval();
    }

    /**
     * Start periodic cleanup of expired auth headers
     */
    private static startCleanupInterval() {
        if (this.cleanupInterval) return;

        this.cleanupInterval = setInterval(() => {
            const now = Date.now();
            for (const [threadId, entry] of this.requestHeaders.entries()) {
                if (now - entry.timestamp > AUTH_HEADERS_TTL_MS) {
                    this.requestHeaders.delete(threadId);
                }
            }
        }, 60 * 1000); // Run cleanup every minute

        // Don't prevent Node.js from exiting
        if (this.cleanupInterval.unref) {
            this.cleanupInterval.unref();
        }
    }

    static setAuthHeaders(threadId: string, headers: Record<string, string>) {
        this.requestHeaders.set(threadId, { headers, timestamp: Date.now() });
    }

    static clearAuthHeaders(threadId: string) {
        this.requestHeaders.delete(threadId);
    }

    protected requestInit(input: RunAgentInput): RequestInit {
        const baseInit = super.requestInit(input);
        const entry = AuthenticatedHttpAgent.requestHeaders.get(input.threadId);
        const authHeaders = entry?.headers || {};

        return {
            ...baseInit,
            headers: {
                ...authHeaders,
                ...(baseInit.headers as Record<string, string>),
            },
        };
    }
}

wintry olive
#

Hey @rigid moon we just fixed a few other things, and I tried to back up to the most simple example possible. Instead of adding a tag to call the EventLogger, I just dropped it into my dashboard component like this:

 export function InsightsDashboard() {
...
  // Initialize CopilotKit event logger
  EventLogger();

And it works here. Not sure why it didn't when I put in <EventLogger />, but this use of useAgent worked for errors and the rest.

#

I think you can take the useAgent() issue off the list (clearly something else was wrong on my end for that).

I still don't have a callback in <Copilotkit's onError> working. but at least there's something that works.

magic otterBOT
#

Noted, We'll look into the <CopilotKit onError> callback issue and get back to you. Thanks for the workaround you shared!