CopilotKit

Pausing the Agent for Input

Pause an agent run mid-tool, hand control to a custom React component, and resume with the user's answer.


"""LlamaIndex scheduling agent -- interrupt-adapted.This agent powers two demos (gen-ui-interrupt, interrupt-headless) that in theLangGraph showcase rely on the native ``interrupt()`` primitive withcheckpoint/resume. LlamaIndex does NOT have that primitive, so we adapt bydelegating the time-picker interaction to a **frontend tool** that the agentcalls by name (``schedule_meeting``). The frontend registers the tool via``useFrontendTool`` with an async handler; that handler renders the interactivepicker, waits for the user to choose a slot (or cancel), and resolves the toolcall with the result.The backend provides a stub ``schedule_meeting`` tool so the LlamaIndexAGUIChatWorkflow emits the proper AG-UI TOOL_CALL_CHUNK events. Actualexecution happens on the frontend; the stub is never invoked becauseCopilotKit intercepts the tool call before the backend can process the result.See ``src/agents/hitl_in_chat_agent.py`` for the related ``book_call`` patternused by the HITL-in-chat demos in this package."""from __future__ import annotationsimport osfrom llama_index.core.tools import FunctionToolfrom llama_index.llms.openai import OpenAIfrom llama_index.protocols.ag_ui.router import get_ag_ui_workflow_routerfrom agents.hitl_in_chat_agent import FixedAGUIChatWorkflow_openai_kwargs = {}if os.environ.get("OPENAI_BASE_URL"):    _openai_kwargs["api_base"] = os.environ["OPENAI_BASE_URL"]SYSTEM_PROMPT = (    "You are a scheduling assistant. Whenever the user asks you to book a call "    "or schedule a meeting, you MUST call the `schedule_meeting` tool. Pass a "    "short `topic` describing the purpose of the meeting and, if known, an "    "`attendee` describing who the meeting is with.\n\n"    "The `schedule_meeting` tool is implemented on the client: it surfaces a "    "time-picker UI to the user and returns the user's selection. After the "    "tool returns, briefly confirm whether the meeting was scheduled and at "    "what time, or note that the user cancelled. Do NOT ask for approval "    "yourself -- always call the tool and let the picker handle the decision.\n\n"    "Keep responses short and friendly. After you finish executing tools, "    "always send a brief final assistant message summarizing what happened so "    "the message persists.")def _schedule_meeting_stub(topic: str, attendee: str = "") -> str:    """Ask the user to pick a time slot for a meeting.    The picker UI presents fixed candidate slots; the user's choice is    returned to the agent.    """    # Frontend-only tool -- CopilotKit intercepts the call and renders the    # TimePickerCard. This stub satisfies the AGUIChatWorkflow tool registry    # so the proper AG-UI events are emitted.    return ""_schedule_meeting_tool = FunctionTool.from_defaults(    fn=_schedule_meeting_stub,    name="schedule_meeting",    description=(        "Ask the user to pick a time slot for a meeting. Pass a short "        "`topic` and optional `attendee`. The picker UI presents fixed "        "candidate slots; the user's choice is returned to the agent."    ),)async def _workflow_factory():    return FixedAGUIChatWorkflow(        llm=OpenAI(model="gpt-4o-mini", **_openai_kwargs),        frontend_tools=[_schedule_meeting_tool],        backend_tools=[],        system_prompt=SYSTEM_PROMPT,        initial_state={},    )interrupt_router = get_ag_ui_workflow_router(    workflow_factory=_workflow_factory,)

What is this?#

useInterrupt lets your agent pause mid-run, hand control to the user through a custom React component, and resume with whatever the user returns. How that pause is implemented depends on the framework's runtime.

The Microsoft Agent Framework runtime can't pause a run mid-tool the way LangGraph's interrupt() does, so this demo uses useFrontendTool with a Promise-based handler instead. The agent calls schedule_meeting like any other tool; the client-side handler renders the picker, holds the request open, and only resolves the Promise once the user picks a slot or cancels. Same UX from the reader's perspective — agent pauses, user answers, agent resumes — different mechanism underneath.

When should I use this?#

Reach for useInterrupt when the pause is a graph-enforced checkpoint where the code path must stop and wait for a human, not an LLM-initiated tool call. Typical cases:

  • A sensitive action (payments, irreversible writes) must be approved
  • A required piece of state isn't known and can only be collected from the user
  • The agent explicitly reaches an approval node in a longer workflow
  • You want the server-side contract to be interrupt(...) and resume with a payload

For LLM-initiated pauses where the model decides on the fly to ask the user, prefer useHumanInTheLoop.

The frontend: useFrontendTool with a Promise-resolving handler#

The handler stores its resolve callback in a ref, returns a Promise that the user's pick eventually resolves, and renders the picker inline in the chat. This is the MS Agent equivalent of useInterrupt's event / resolve pair:

page.tsx
  useFrontendTool({    name: "schedule_meeting",    description:      "Ask the user to pick a time slot for a meeting via an in-chat " +      "picker. Blocks until the user chooses a slot or cancels.",    parameters: z.object({      topic: z        .string()        .describe("Short human-readable description of the meeting."),      attendee: z        .string()        .optional()        .describe("Who the meeting is with (optional)."),    }),    // Async handler: returns a Promise that resolves only once the user    // acts on the picker. This is the LlamaIndex shim for LangGraph's    // `interrupt()`/`resolve()` pair.    handler: async (): Promise<string> => {      const result = await new Promise<PickerResult>((resolve) => {        resolverRef.current = resolve;      });      if ("cancelled" in result && result.cancelled) {        return "User cancelled. Meeting NOT scheduled.";      }      if ("chosen_label" in result) {        return `Meeting scheduled for ${result.chosen_label}.`;      }      return "User did not pick a time. Meeting NOT scheduled.";    },    render: ({ args, status }) => {      if (status === "complete") return null;      const topic =        (args as { topic?: string } | undefined)?.topic ?? "a meeting";      const attendee = (args as { attendee?: string } | undefined)?.attendee;      return (        <TimePickerCard          topic={topic}          attendee={attendee}          slots={DEFAULT_SLOTS}          onSubmit={(result) => {            const fn = resolverRef.current;            resolverRef.current = null;            fn?.(result);          }}        />      );    },  });

The backend: agent instructed to call the frontend tool#

The agent has no local schedule_meeting implementation — the tool is registered entirely on the frontend. The backend's only job is to instruct the model to call schedule_meeting whenever the user wants to book a meeting. AG-UI routes the tool call to the client, where the Promise-returning handler takes over:

interrupt_agent.py
SYSTEM_PROMPT = (    "You are a scheduling assistant. Whenever the user asks you to book a call "    "or schedule a meeting, you MUST call the `schedule_meeting` tool. Pass a "    "short `topic` describing the purpose of the meeting and, if known, an "    "`attendee` describing who the meeting is with.\n\n"    "The `schedule_meeting` tool is implemented on the client: it surfaces a "    "time-picker UI to the user and returns the user's selection. After the "    "tool returns, briefly confirm whether the meeting was scheduled and at "    "what time, or note that the user cancelled. Do NOT ask for approval "    "yourself -- always call the tool and let the picker handle the decision.\n\n"    "Keep responses short and friendly. After you finish executing tools, "    "always send a brief final assistant message summarizing what happened so "    "the message persists.")def _schedule_meeting_stub(topic: str, attendee: str = "") -> str:    """Ask the user to pick a time slot for a meeting.    The picker UI presents fixed candidate slots; the user's choice is    returned to the agent.    """    # Frontend-only tool -- CopilotKit intercepts the call and renders the    # TimePickerCard. This stub satisfies the AGUIChatWorkflow tool registry    # so the proper AG-UI events are emitted.    return ""_schedule_meeting_tool = FunctionTool.from_defaults(    fn=_schedule_meeting_stub,    name="schedule_meeting",    description=(        "Ask the user to pick a time slot for a meeting. Pass a short "        "`topic` and optional `attendee`. The picker UI presents fixed "        "candidate slots; the user's choice is returned to the agent."    ),)async def _workflow_factory():    return FixedAGUIChatWorkflow(        llm=OpenAI(model="gpt-4o-mini", **_openai_kwargs),        frontend_tools=[_schedule_meeting_tool],        backend_tools=[],        system_prompt=SYSTEM_PROMPT,        initial_state={},    )interrupt_router = get_ag_ui_workflow_router(    workflow_factory=_workflow_factory,)

Going further#