Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ export function CopilotKitInternal(cpkProps: CopilotKitProps) {
export const defaultCopilotContextCategories = ["global"];

function entryPointsToFunctionCallHandler(actions: FrontendAction<any>[]): FunctionCallHandler {
return async ({ name, args }: { name: string; args: Record<string, any> }) => {
return async ({ name, args, executionId }: { name: string; args: Record<string, any>; executionId?: string }) => {
let actionsByFunctionName: Record<string, FrontendAction<any>> = {};
for (let action of actions) {
actionsByFunctionName[action.name] = action;
Expand All @@ -587,7 +587,9 @@ function entryPointsToFunctionCallHandler(actions: FrontendAction<any>[]): Funct
await new Promise<void>((resolve, reject) => {
flushSync(async () => {
try {
result = await action.handler?.(args);
// 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);
Expand Down
24 changes: 12 additions & 12 deletions CopilotKit/packages/react-core/src/hooks/use-copilot-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,24 +228,24 @@ export function useCopilotAction<const T extends Parameter[] | [] = []>(
// 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<T> : ActionRenderPropsWait<T>;

// Type guard to check if renderAndWait is for no args case
Expand Down
3 changes: 2 additions & 1 deletion CopilotKit/packages/runtime/src/service-adapters/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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$,
Expand Down
11 changes: 9 additions & 2 deletions CopilotKit/packages/shared/src/types/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,20 @@ export type MappedParameterTypes<T extends Parameter[] | [] = []> = T extends []
| BaseParameterType<P>;
};

export type ActionHandlerMeta = {
/** Execution identifier for the current action call */
executionId?: string;
};

export type Action<T extends Parameter[] | [] = []> = {
name: string;
description?: string;
parameters?: T;
handler?: T extends []
? () => any | Promise<any>
: (args: MappedParameterTypes<T>) => any | Promise<any>;
? // 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>) | (() => any | Promise<any>)
: (args: MappedParameterTypes<T>, meta?: ActionHandlerMeta) => any | Promise<any>;
};

// This is the original "ceiling is being raised" version of MappedParameterTypes.
Expand Down
2 changes: 2 additions & 0 deletions CopilotKit/packages/shared/src/types/openai-assistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/reference/hooks/useCopilotAction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ This hooks enables you to dynamically generate UI elements and render them in th
The name of the action.
</PropertyReference>

<PropertyReference name="handler" type="(args) => Promise<any>" required>
The handler of the action.
<PropertyReference name="handler" type="(args, meta?) => Promise<any>" required>
The handler of the action. An optional <code>meta</code> object is provided containing <code>executionId</code> to help you dedupe executions across re-renders.
</PropertyReference>

<PropertyReference name="description" type="string">
Expand Down