Skip to content

Commit 74447ff

Browse files
tylerslatonjpr5
authored andcommitted
fix(vue): CR correctness fixes for v1 hooks and A2UI catalog
Fix use-copilot-action and use-copilot-readable v1 hook implementations and A2UI catalog registration logic based on code review findings.
1 parent a187e6b commit 74447ff

3 files changed

Lines changed: 1101 additions & 0 deletions

File tree

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/**
2+
* V1 compatibility wrapper for useCopilotAction.
3+
*
4+
* Accepts the legacy Parameter[] action format and routes to the appropriate
5+
* v2 composable (useFrontendTool, useHumanInTheLoop, or useRenderTool).
6+
*/
7+
import type { WatchSource } from "vue";
8+
import type { Parameter, MappedParameterTypes } from "@copilotkit/shared";
9+
import { getZodParameters, parseJson } from "@copilotkit/shared";
10+
import { useFrontendTool as useFrontendToolV2 } from "../v2/hooks/use-frontend-tool";
11+
import { useHumanInTheLoop as useHumanInTheLoopV2 } from "../v2/hooks/use-human-in-the-loop";
12+
import { useRenderTool as useRenderToolV2 } from "../v2/hooks/use-render-tool";
13+
import type { VueFrontendTool, VueHumanInTheLoop } from "../v2/types";
14+
15+
// Wraps a v1 render function so a JSON-string `result` is parsed before being
16+
// passed through. Mirrors the v1 React behavior. If render is a Component
17+
// (object) rather than a function, returns it unchanged — Components receive
18+
// props through Vue's prop system and the user is responsible for parsing.
19+
function wrapRenderWithJsonResult<R>(render: R): R {
20+
if (typeof render !== "function") return render;
21+
return ((props: { result?: unknown }) => {
22+
const next =
23+
typeof props.result === "string"
24+
? { ...props, result: parseJson(props.result, props.result) }
25+
: props;
26+
return (render as (p: unknown) => unknown)(next);
27+
}) as R;
28+
}
29+
30+
export interface FrontendAction<T extends Parameter[] | [] = []> {
31+
name: string;
32+
description?: string;
33+
parameters?: T;
34+
handler?: (args: MappedParameterTypes<T>) => unknown | Promise<unknown>;
35+
followUp?: boolean;
36+
available?: "disabled" | "enabled" | "remote" | "frontend";
37+
render?: VueFrontendTool<MappedParameterTypes<T>>["render"];
38+
renderAndWaitForResponse?: VueFrontendTool<MappedParameterTypes<T>>["render"];
39+
renderAndWait?: VueFrontendTool<MappedParameterTypes<T>>["render"];
40+
agentId?: string;
41+
}
42+
43+
export interface CatchAllFrontendAction {
44+
name: "*";
45+
render: (props: unknown) => unknown;
46+
}
47+
48+
export function useCopilotAction<const T extends Parameter[] | [] = []>(
49+
action: FrontendAction<T> | CatchAllFrontendAction,
50+
deps?: WatchSource<unknown>[],
51+
): void {
52+
const zodParameters =
53+
"parameters" in action
54+
? getZodParameters(action.parameters as T)
55+
: undefined;
56+
57+
// Catch-all render action
58+
if (action.name === "*") {
59+
useRenderToolV2(
60+
{
61+
name: "*",
62+
render: wrapRenderWithJsonResult(
63+
(action as CatchAllFrontendAction).render,
64+
),
65+
...("agentId" in action
66+
? { agentId: (action as FrontendAction<T>).agentId }
67+
: {}),
68+
},
69+
deps,
70+
);
71+
return;
72+
}
73+
74+
const typedAction = action as FrontendAction<T>;
75+
76+
// Human-in-the-loop: has renderAndWaitForResponse or renderAndWait
77+
if (
78+
"renderAndWaitForResponse" in typedAction ||
79+
"renderAndWait" in typedAction
80+
) {
81+
const render =
82+
typedAction.render ??
83+
typedAction.renderAndWaitForResponse ??
84+
typedAction.renderAndWait;
85+
86+
if (!render) {
87+
console.warn(
88+
`[CopilotKit] useCopilotAction: HITL action '${typedAction.name}' ` +
89+
`has no render function. Skipping.`,
90+
);
91+
return;
92+
}
93+
94+
useHumanInTheLoopV2<MappedParameterTypes<T>>(
95+
{
96+
name: typedAction.name,
97+
description: typedAction.description,
98+
parameters: zodParameters,
99+
render: wrapRenderWithJsonResult(render) as VueHumanInTheLoop<
100+
MappedParameterTypes<T>
101+
>["render"],
102+
agentId: typedAction.agentId,
103+
},
104+
deps,
105+
);
106+
return;
107+
}
108+
109+
// Render-only: available is "frontend" or "disabled" (no handler invoked remotely)
110+
if (
111+
typedAction.available === "frontend" ||
112+
typedAction.available === "disabled"
113+
) {
114+
if (typedAction.render && zodParameters) {
115+
useRenderToolV2(
116+
{
117+
name: typedAction.name,
118+
parameters: zodParameters,
119+
render: wrapRenderWithJsonResult(
120+
typedAction.render as (props: unknown) => unknown,
121+
),
122+
agentId: typedAction.agentId,
123+
},
124+
deps,
125+
);
126+
} else {
127+
console.warn(
128+
`[CopilotKit] useCopilotAction: action '${typedAction.name}' ` +
129+
`with available="${typedAction.available}" requires both ` +
130+
`'render' and 'parameters'. Skipping registration.`,
131+
);
132+
}
133+
return;
134+
}
135+
136+
// Default: frontend tool with handler
137+
// Wrap the v1 handler (single-arg) to match v2's (args, context) => Promise<unknown> signature
138+
const normalizedHandler = typedAction.handler
139+
? (args: MappedParameterTypes<T>) =>
140+
Promise.resolve(typedAction.handler!(args))
141+
: undefined;
142+
143+
// Convert v1 available (string enum) to v2 available (boolean)
144+
// At this point, "frontend" and "disabled" have been handled above,
145+
// so remaining values are "enabled", "remote", or undefined.
146+
// "remote" means server-only: register the tool but mark it as not
147+
// available on the frontend (matches React's ActionInputAvailability.Remote).
148+
let normalizedAvailable: boolean | undefined;
149+
if (typedAction.available === "remote") {
150+
normalizedAvailable = false;
151+
} else if (typedAction.available !== undefined) {
152+
normalizedAvailable = true;
153+
}
154+
155+
useFrontendToolV2<MappedParameterTypes<T>>(
156+
{
157+
name: typedAction.name,
158+
description: typedAction.description,
159+
parameters: zodParameters,
160+
handler: normalizedHandler,
161+
followUp: typedAction.followUp,
162+
render: wrapRenderWithJsonResult(typedAction.render),
163+
available: normalizedAvailable,
164+
agentId: typedAction.agentId,
165+
},
166+
deps,
167+
);
168+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* V1 compatibility wrapper for useCopilotReadable.
3+
*
4+
* Provides app-state and other information to the Copilot context.
5+
* Delegates directly to the v2 CopilotKitCoreVue instance.
6+
*/
7+
import { watch, ref, type Ref } from "vue";
8+
import type { WatchSource } from "vue";
9+
import { useCopilotKit } from "../v2/providers/useCopilotKit";
10+
11+
export interface UseCopilotReadableOptions {
12+
/** The description of the information to be added to the Copilot context. */
13+
description: string;
14+
/** The value to be added to the Copilot context. Object values are automatically stringified. */
15+
value: unknown;
16+
/** Whether the context is available to the Copilot. */
17+
available?: "enabled" | "disabled";
18+
/** Custom conversion function to serialize the value to a string. */
19+
convert?: (description: string, value: unknown) => string;
20+
}
21+
22+
export function useCopilotReadable(
23+
options: UseCopilotReadableOptions,
24+
deps?: WatchSource<unknown>[],
25+
): Ref<string | undefined> {
26+
const { copilotkit } = useCopilotKit();
27+
const ctxIdRef = ref<string | undefined>(undefined);
28+
29+
const extraDeps = deps ?? [];
30+
31+
watch(
32+
[
33+
() => options.description,
34+
() => options.value,
35+
() => options.convert,
36+
() => options.available,
37+
...extraDeps,
38+
],
39+
(_newValues, _old, onCleanup) => {
40+
const core = copilotkit.value;
41+
if (!core) return;
42+
43+
const { description, value, convert, available } = options;
44+
45+
let serializedValue: string;
46+
try {
47+
serializedValue = convert
48+
? convert(description, value)
49+
: JSON.stringify(value);
50+
} catch (err) {
51+
console.warn(
52+
`[CopilotKit] useCopilotReadable: failed to serialize ` +
53+
`value for "${description}":`,
54+
err,
55+
);
56+
serializedValue = String(value);
57+
}
58+
59+
if (available === "disabled") return;
60+
61+
ctxIdRef.value = core.addContext({
62+
description,
63+
value: serializedValue,
64+
});
65+
66+
onCleanup(() => {
67+
if (!ctxIdRef.value) return;
68+
core.removeContext(ctxIdRef.value);
69+
});
70+
},
71+
{ immediate: true },
72+
);
73+
74+
return ctxIdRef;
75+
}

0 commit comments

Comments
 (0)