From 37ec12a233890d00c179b17d209c024aec90e6ac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 8 Oct 2025 11:15:11 +0000 Subject: [PATCH 1/2] feat: Pass executionId to action handlers Co-authored-by: elcngchunyik --- .../src/components/copilot-provider/copilotkit.tsx | 7 +++++-- CopilotKit/packages/react-core/src/hooks/use-chat.ts | 1 + .../packages/runtime/src/service-adapters/events.ts | 3 ++- CopilotKit/packages/shared/src/types/action.ts | 11 +++++++++-- .../packages/shared/src/types/openai-assistant.ts | 2 ++ .../content/docs/reference/hooks/useCopilotAction.mdx | 4 ++-- 6 files changed, 21 insertions(+), 7 deletions(-) diff --git a/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx b/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx index b116f39028d..f405111d380 100644 --- a/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx +++ b/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx @@ -575,7 +575,7 @@ export function CopilotKitInternal(cpkProps: CopilotKitProps) { export const defaultCopilotContextCategories = ["global"]; function entryPointsToFunctionCallHandler(actions: FrontendAction[]): FunctionCallHandler { - return async ({ name, args }: { name: string; args: Record }) => { + return async ({ name, args, executionId }: { name: string; args: Record; executionId?: string }) => { let actionsByFunctionName: Record> = {}; for (let action of actions) { actionsByFunctionName[action.name] = action; @@ -587,7 +587,10 @@ function entryPointsToFunctionCallHandler(actions: FrontendAction[]): Funct await new Promise((resolve, reject) => { flushSync(async () => { try { - result = await action.handler?.(args); + // Always pass args and optional meta with executionId. For no-args handlers, + // extra parameters are ignored at runtime and remain backward compatible. + // Using `as any` to avoid complex conditional typing here. + result = await (action.handler as any)?.(args, { executionId }); resolve(); } catch (error) { reject(error); diff --git a/CopilotKit/packages/react-core/src/hooks/use-chat.ts b/CopilotKit/packages/react-core/src/hooks/use-chat.ts index 3acb1be51d4..f604e70f1cb 100644 --- a/CopilotKit/packages/react-core/src/hooks/use-chat.ts +++ b/CopilotKit/packages/react-core/src/hooks/use-chat.ts @@ -1080,6 +1080,7 @@ async function executeAction({ messages: currentMessagesForHandler, name: message.name, args: message.arguments, + executionId: message.id, }); // For HITL actions, call flushSync immediately after their handler has set up the promise diff --git a/CopilotKit/packages/runtime/src/service-adapters/events.ts b/CopilotKit/packages/runtime/src/service-adapters/events.ts index d703fe14c3c..869ca674488 100644 --- a/CopilotKit/packages/runtime/src/service-adapters/events.ts +++ b/CopilotKit/packages/runtime/src/service-adapters/events.ts @@ -492,7 +492,8 @@ async function executeAction( } else { // call the function try { - const result = await action.handler?.(args); + // Pass the executionId so handlers can dedupe across re-renders if needed + const result = await (action.handler as any)?.(args, { executionId: actionExecutionId }); await streamLangChainResponse({ result, eventStream$, diff --git a/CopilotKit/packages/shared/src/types/action.ts b/CopilotKit/packages/shared/src/types/action.ts index 7eb84db1103..f0d725fd490 100644 --- a/CopilotKit/packages/shared/src/types/action.ts +++ b/CopilotKit/packages/shared/src/types/action.ts @@ -78,13 +78,20 @@ export type MappedParameterTypes = T extends [] | BaseParameterType

; }; +export type ActionHandlerMeta = { + /** Execution identifier for the current action call */ + executionId?: string; +}; + export type Action = { name: string; description?: string; parameters?: T; handler?: T extends [] - ? () => any | Promise - : (args: MappedParameterTypes) => any | Promise; + ? // No-args handlers remain callable without parameters for backward compatibility, + // but may optionally receive a metadata object containing the executionId. + ((meta?: ActionHandlerMeta) => any | Promise) | (() => any | Promise) + : (args: MappedParameterTypes, meta?: ActionHandlerMeta) => any | Promise; }; // This is the original "ceiling is being raised" version of MappedParameterTypes. diff --git a/CopilotKit/packages/shared/src/types/openai-assistant.ts b/CopilotKit/packages/shared/src/types/openai-assistant.ts index fbff0f1d62d..3a748a6b072 100644 --- a/CopilotKit/packages/shared/src/types/openai-assistant.ts +++ b/CopilotKit/packages/shared/src/types/openai-assistant.ts @@ -30,6 +30,8 @@ export interface FunctionCallHandlerArguments { messages: any[]; name: string; args: any; + /** Optional execution identifier for this function/action call, useful for deduping */ + executionId?: string; } export type FunctionCallHandler = (args: FunctionCallHandlerArguments) => Promise; diff --git a/docs/content/docs/reference/hooks/useCopilotAction.mdx b/docs/content/docs/reference/hooks/useCopilotAction.mdx index 84041366b20..a705321f569 100644 --- a/docs/content/docs/reference/hooks/useCopilotAction.mdx +++ b/docs/content/docs/reference/hooks/useCopilotAction.mdx @@ -71,8 +71,8 @@ This hooks enables you to dynamically generate UI elements and render them in th The name of the action. - - The handler of the action. + + The handler of the action. An optional meta object is provided containing executionId to help you dedupe executions across re-renders. From 31de2aba39ffba35c7986037c4713e7f0645c1c5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 8 Oct 2025 11:46:09 +0000 Subject: [PATCH 2/2] Refactor: Pass executionId to action handlers correctly Co-authored-by: elcngchunyik --- .../copilot-provider/copilotkit.tsx | 7 +++--- .../packages/react-core/src/hooks/use-chat.ts | 1 - .../src/hooks/use-copilot-action.ts | 24 +++++++++---------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx b/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx index f405111d380..636a0132fea 100644 --- a/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx +++ b/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx @@ -587,10 +587,9 @@ function entryPointsToFunctionCallHandler(actions: FrontendAction[]): Funct await new Promise((resolve, reject) => { flushSync(async () => { try { - // Always pass args and optional meta with executionId. For no-args handlers, - // extra parameters are ignored at runtime and remain backward compatible. - // Using `as any` to avoid complex conditional typing here. - result = await (action.handler as any)?.(args, { executionId }); + // Prefer executionId sourced from the action render (HITL), otherwise from the backend callback meta. + const effectiveExecutionId = (action as any)._executionId || executionId; + result = await (action.handler as any)?.(args, { executionId: effectiveExecutionId }); resolve(); } catch (error) { reject(error); diff --git a/CopilotKit/packages/react-core/src/hooks/use-chat.ts b/CopilotKit/packages/react-core/src/hooks/use-chat.ts index f604e70f1cb..3acb1be51d4 100644 --- a/CopilotKit/packages/react-core/src/hooks/use-chat.ts +++ b/CopilotKit/packages/react-core/src/hooks/use-chat.ts @@ -1080,7 +1080,6 @@ async function executeAction({ messages: currentMessagesForHandler, name: message.name, args: message.arguments, - executionId: message.id, }); // For HITL actions, call flushSync immediately after their handler has set up the promise diff --git a/CopilotKit/packages/react-core/src/hooks/use-copilot-action.ts b/CopilotKit/packages/react-core/src/hooks/use-copilot-action.ts index 1dd03cda3a6..ae9c63afb95 100644 --- a/CopilotKit/packages/react-core/src/hooks/use-copilot-action.ts +++ b/CopilotKit/packages/react-core/src/hooks/use-copilot-action.ts @@ -228,24 +228,24 @@ export function useCopilotAction( // If conditions met, status remains 'executing' } // Create type safe waitProps based on whether T extends empty array or not + const isActiveExecutingInstance = + status === "executing" && + renderAndWaitRef.current && + renderAndWaitRef.current.messageId === currentRenderMessageId; + + // Expose the current execution id on the action so the provider can pass it to the handler meta + if (isActiveExecutingInstance && currentRenderMessageId) { + (action as any)._executionId = currentRenderMessageId; + } + const waitProps = { status, args: props.args, result: props.result, // handler and respond should only be provided if this is the truly active instance // and its promise infrastructure is ready. - handler: - status === "executing" && - renderAndWaitRef.current && - renderAndWaitRef.current.messageId === currentRenderMessageId - ? renderAndWaitRef.current!.resolve - : undefined, - respond: - status === "executing" && - renderAndWaitRef.current && - renderAndWaitRef.current.messageId === currentRenderMessageId - ? renderAndWaitRef.current!.resolve - : undefined, + handler: isActiveExecutingInstance ? renderAndWaitRef.current!.resolve : undefined, + respond: isActiveExecutingInstance ? renderAndWaitRef.current!.resolve : undefined, } as T extends [] ? ActionRenderPropsNoArgsWait : ActionRenderPropsWait; // Type guard to check if renderAndWait is for no args case