Back to Copilotkit

useHumanInTheLoop

showcase/shell-docs/src/content/reference/vue/hooks/useHumanInTheLoop.mdx

1.61.113.5 KB
Original Source

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.

<Callout type="info"> `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. </Callout>

Signature

ts
import { useHumanInTheLoop } from "@copilotkit/vue/v2";

function useHumanInTheLoop<T extends Record<string, unknown>>(
  tool: VueHumanInTheLoop<T>,
  deps?: WatchSource<unknown>[],
): void;

Parameters

<PropertyReference name="tool" type="VueHumanInTheLoop<T>" required> The interactive tool definition. It accepts the same fields as a frontend tool except `handler` (which is managed internally), plus a required `render` component. <PropertyReference name="name" type="string" required> A unique name for the tool. The agent references this name when it needs human input. </PropertyReference> <PropertyReference name="description" type="string"> A natural-language description that tells the agent what the tool does and when to invoke it (e.g., "Ask the user to confirm before deleting records"). </PropertyReference> <PropertyReference name="parameters" type="StandardSchemaV1 (e.g. z.ZodSchema)"> A schema (typically a Zod schema) defining the arguments the agent passes to the tool. These arguments are forwarded to the render component as the `args` prop so you can build context-aware UI. </PropertyReference> <PropertyReference name="render" type="VueHumanInTheLoopRenderFn<T>" required> A Vue component (or render function) that drives the human interaction. It receives the following props, whose shape depends on the current `status`:
**Always present:**
- `name: string` -- the tool name
- `description: string` -- the tool description (an empty string when the tool defines none)
- `toolCallId: string` -- the unique id of this tool call
- `status: ToolCallStatus` -- the current phase (`"inProgress"`, `"executing"`, or `"complete"`)

**When `status` is `ToolCallStatus.InProgress` (`"inProgress"`):**
- `args: Partial<T>` -- partially streamed arguments
- `result: undefined`
- `respond: undefined` -- not yet available

**When `status` is `ToolCallStatus.Executing` (`"executing"`):**
- `args: T` -- fully resolved arguments
- `result: undefined`
- `respond: (result: unknown) => Promise<void>` -- call this to send the user's response back to the agent and resume execution

**When `status` is `ToolCallStatus.Complete` (`"complete"`):**
- `args: T` -- the original arguments
- `result: string` -- the serialized result
- `respond: undefined` -- no longer available
</PropertyReference> <PropertyReference name="available" type="boolean" default="true"> Whether the tool is available to the agent. Set to `false` to hide the tool from the agent without unregistering it. </PropertyReference> <PropertyReference name="agentId" type="string"> Optional agent ID to constrain this tool to a specific agent. If specified, the tool is only available to that agent. </PropertyReference> <PropertyReference name="followUp" type="boolean"> Whether the agent should automatically follow up after the tool responds. </PropertyReference> </PropertyReference> <PropertyReference name="deps" type="WatchSource<unknown>[]"> An optional array of reactive watch sources (refs, getters, etc.). When provided, the tool registration is refreshed whenever any source changes. </PropertyReference>

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):

vue
<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:

vue
<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:

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:

vue
<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:

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:

vue
<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 respond is 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, respond available), and complete (user has responded, result available). Your render component receives the appropriate props for each phase.
  • respond is only passed during executing: In the inProgress and complete phases, respond is undefined. Guard on status === ToolCallStatus.Executing && respond before calling it.
  • Single response: Calling respond resolves 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 a VueFrontendTool (your tool plus the generated handler and a wrapping render component) to useFrontendTool, 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 underlying useFrontendTool watch's cleanup.
  • No return value: The composable returns void.
<Cards> <Card title="useFrontendTool" href="/reference/vue/hooks/useFrontendTool" description="Register tools with automated handlers that do not require user interaction." /> <Card title="useCopilotKit" href="/reference/vue/hooks/useCopilotKit" description="Access the underlying CopilotKit instance." /> <Card title="CopilotKitProvider" href="/reference/vue/components/CopilotKitProvider" description="Provides the CopilotKit instance to descendant composables." /> </Cards>

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.

ValueString valueDescription
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.