useHumanInTheLoop
Vue 3 composable for interactive tools that pause agent execution and wait for user input
Overview
useHumanInTheLoop registers an interactive frontend tool that pauses agent execution until the user responds through your custom UI. Unlike useFrontendTool, you do not supply a handler. Instead, the composable manages an internal status machine (InProgress -> Executing -> Complete) and passes a respond callback to your render component while the tool is in the executing phase. The agent stays paused until respond is called with the user's input.
Internally the composable wraps useFrontendTool. It generates a handler that returns a promise, holds onto that promise's resolve in a ref, and resolves it when respond is invoked, which sends the result back to the agent and resumes execution. Use it for confirmation dialogs, approval workflows, form collection, or any scenario where a human must provide input before the agent can continue.
useHumanInTheLoop must be called within a component that has access to the
CopilotKit instance provided by CopilotKitProvider. Like all Vue
composables, call it synchronously inside <script setup> or a setup()
function so its reactive scope is bound to the component lifecycle. The tool
is unregistered automatically on scope cleanup.
Signature
import { useHumanInTheLoop } from "@copilotkit/vue/v2";
function useHumanInTheLoop<T extends Record<string, unknown>>(
tool: VueHumanInTheLoop<T>,
deps?: WatchSource<unknown>[],
): void;Parameters
Prop
Type
Prop
Type
Usage
Define the render component as a separate Vue SFC, then register it with useHumanInTheLoop. The component receives status, args, respond, name, and description as props.
Confirmation Dialog
A common pattern where the agent asks the user to confirm a destructive action.
ConfirmDeletion.vue (the render component):
<script setup lang="ts">
import { ToolCallStatus } from "@copilotkit/core";
defineProps<{
status: ToolCallStatus;
args: { itemName?: string; itemCount?: number };
name: string;
description: string;
result?: string;
respond?: (result: unknown) => Promise<void>;
}>();
</script>
<template>
<div v-if="status === ToolCallStatus.InProgress" class="p-4 text-gray-500">
Preparing confirmation...
</div>
<div
v-else-if="status === ToolCallStatus.Executing && respond"
class="p-4 border rounded"
>
<p>Are you sure you want to delete {{ args.itemCount }} {{ args.itemName }}(s)?</p>
<div class="flex gap-2 mt-4">
<button
class="bg-red-500 text-white px-4 py-2 rounded"
@click="respond({ confirmed: true })"
>
Delete
</button>
<button
class="bg-gray-300 px-4 py-2 rounded"
@click="respond({ confirmed: false })"
>
Cancel
</button>
</div>
</div>
<div
v-else-if="status === ToolCallStatus.Complete && result"
class="p-2 text-sm text-gray-600"
>
{{ JSON.parse(result).confirmed ? "Items deleted." : "Deletion cancelled." }}
</div>
</template>Register it from a component in scope of CopilotKitProvider:
<script setup lang="ts">
import { useHumanInTheLoop } from "@copilotkit/vue/v2";
import { z } from "zod";
import ConfirmDeletion from "./ConfirmDeletion.vue";
useHumanInTheLoop({
name: "confirmDeletion",
description: "Ask the user to confirm before deleting items",
parameters: z.object({
itemName: z.string().describe("Name of the item to delete"),
itemCount: z.number().describe("Number of items to delete"),
}),
render: ConfirmDeletion,
});
</script>
<template>
<!-- nothing to render here; the tool UI is shown in the chat -->
</template>Form Input Collection
Collect structured input from the user before the agent proceeds. Local form state lives in the render component using ref.
ShippingAddressForm.vue:
<script setup lang="ts">
import { ref } from "vue";
import { ToolCallStatus } from "@copilotkit/core";
defineProps<{
status: ToolCallStatus;
args: { orderSummary?: string };
name: string;
description: string;
result?: string;
respond?: (result: unknown) => Promise<void>;
}>();
const address = ref({ street: "", city: "", zip: "" });
</script>
<template>
<div
v-if="status === ToolCallStatus.Executing && respond"
class="p-4 border rounded space-y-3"
>
<p class="font-medium">Order: {{ args.orderSummary }}</p>
<p>Please enter your shipping address:</p>
<input
v-model="address.street"
placeholder="Street address"
class="w-full border p-2 rounded"
/>
<input
v-model="address.city"
placeholder="City"
class="w-full border p-2 rounded"
/>
<input
v-model="address.zip"
placeholder="ZIP code"
class="w-full border p-2 rounded"
/>
<button
class="bg-blue-500 text-white px-4 py-2 rounded"
@click="respond({ ...address })"
>
Submit Address
</button>
</div>
<div v-else-if="status === ToolCallStatus.Complete" class="p-2 text-green-600">
Shipping address submitted.
</div>
</template>Registration:
<script setup lang="ts">
import { useHumanInTheLoop } from "@copilotkit/vue/v2";
import { z } from "zod";
import ShippingAddressForm from "./ShippingAddressForm.vue";
useHumanInTheLoop({
name: "collectShippingAddress",
description: "Collect shipping address from the user before placing an order",
parameters: z.object({
orderSummary: z.string().describe("A summary of the order being placed"),
}),
render: ShippingAddressForm,
});
</script>
<template>
<!-- tool UI renders inside the chat -->
</template>Approval Workflow with Context
ExpenseApproval.vue:
<script setup lang="ts">
import { ToolCallStatus } from "@copilotkit/core";
defineProps<{
status: ToolCallStatus;
args: {
employeeName?: string;
amount?: number;
category?: string;
description?: string;
};
name: string;
description: string;
result?: string;
respond?: (result: unknown) => Promise<void>;
}>();
</script>
<template>
<div
v-if="status === ToolCallStatus.Executing && respond"
class="p-4 border rounded"
>
<h3 class="font-bold">Expense Approval Required</h3>
<div class="mt-2 space-y-1 text-sm">
<p>Employee: {{ args.employeeName }}</p>
<p>Amount: ${{ args.amount?.toFixed(2) }}</p>
<p>Category: {{ args.category }}</p>
<p>Description: {{ args.description }}</p>
</div>
<div class="flex gap-2 mt-4">
<button
class="bg-green-500 text-white px-4 py-2 rounded"
@click="respond({ approved: true })"
>
Approve
</button>
<button
class="bg-red-500 text-white px-4 py-2 rounded"
@click="respond({ approved: false, reason: 'Needs more detail' })"
>
Reject
</button>
</div>
</div>
<div
v-else-if="status === ToolCallStatus.Complete && result"
class="p-2 text-sm"
:class="JSON.parse(result).approved ? 'text-green-600' : 'text-red-600'"
>
{{
JSON.parse(result).approved
? "Expense approved."
: `Expense rejected: ${JSON.parse(result).reason}`
}}
</div>
</template>Registration:
<script setup lang="ts">
import { useHumanInTheLoop } from "@copilotkit/vue/v2";
import { z } from "zod";
import ExpenseApproval from "./ExpenseApproval.vue";
useHumanInTheLoop({
name: "approveExpense",
description: "Request manager approval for an expense report",
parameters: z.object({
employeeName: z.string().describe("Name of the employee"),
amount: z.number().describe("Expense amount in dollars"),
category: z.string().describe("Expense category"),
description: z.string().describe("Description of the expense"),
}),
render: ExpenseApproval,
});
</script>
<template>
<!-- tool UI renders inside the chat -->
</template>Behavior
- Blocks agent execution: The internally generated handler returns a promise that does not resolve until
respondis called, so the agent pauses on this tool call and waits for the user. - Internal status machine: The composable drives three phases -
inProgress(arguments streaming in),executing(waiting for the user,respondavailable), andcomplete(user has responded,resultavailable). Your render component receives the appropriate props for each phase. respondis only passed duringexecuting: In theinProgressandcompletephases,respondisundefined. Guard onstatus === ToolCallStatus.Executing && respondbefore calling it.- Single response: Calling
respondresolves the pending promise once and clears the stored resolver, so subsequent calls are no-ops for that tool invocation. - Built on
useFrontendTool: The composable forwards aVueFrontendTool(your tool plus the generatedhandlerand a wrapping render component) touseFrontendTool, so the same registration lifecycle applies. - Scope lifecycle: The tool and its render component are registered while the composable's reactive scope is active and removed automatically on scope cleanup — the render component via
onScopeDispose, and the tool via the underlyinguseFrontendToolwatch's cleanup. - No return value: The composable returns
void.
Related
useFrontendTool
Register tools with automated handlers that do not require user interaction.
useCopilotKit
Access the underlying CopilotKit instance.
CopilotKitProvider
Provides the CopilotKit instance to descendant composables.
ToolCallStatus
The ToolCallStatus enum is exported from @copilotkit/core and defines the three phases of tool execution. Its string values are what appear in the render component's status prop.
| Value | String value | Description |
|---|---|---|
ToolCallStatus.InProgress | "inProgress" | Arguments are being streamed from the agent. The tool has not started executing yet. |
ToolCallStatus.Executing | "executing" | Arguments are fully resolved. For useHumanInTheLoop, the respond callback is passed. |
ToolCallStatus.Complete | "complete" | Execution is finished. The result string is available. |