Skip to content
This documentation covers the kagent 1.0 alpha. For the latest 0.x release, see the 0.x docs.

For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.

Human in the loop

Page as Markdown

Understand how an agent pauses to ask a question or to get a tool call approved, and what a client does to answer it.

An agent that only answers questions can run unattended. An agent that takes action often should not. The human in the loop (HITL) mechanism lets an agent stop mid-turn, return a question or a pending tool call to a person, and continue once that person answers.

Important

HITL has two halves, and a working setup needs both. An AgentTemplateAgentTemplateA Kubernetes custom resource defining what an agent does: its model, system prompt, tools, skills, and plugins. It runs only once a Harness accepts it.Learn more decides which tool calls pause through requireApproval on a tool binding. The client decides whether it can answer a pause by negotiating the HITL extension on each call. A client that does not request the extension still gets the pause, as the agent stops and the task waits. That client cannot answer, because the request reaches it as bare text with no correlation id.

How a pause works

The following diagram traces one turn in which the agent stops for a person.

    flowchart TB
    caller["Client sends a message<br>requesting the HITL extension"]
    working["Agent works on the turn"]
    decision{"Does the agent need<br>a person?"}
    pause["Task state becomes<br>INPUT_REQUIRED"]
    request["Status message carries a<br>tool_approval_request<br>or ask_user_request"]
    answer["Client sends a response message<br>on the same task"]
    done["Agent finishes the turn"]

    caller --> working
    working --> decision
    decision -->|no| done
    decision -->|yes| pause
    pause --> request
    request --> answer
    answer --> working
  

The client opens the turn by sending a message that requests the HITL extension. The agent works until it either finishes, in which case the turn ends, or needs a person. When it needs a person, the task moves to INPUT_REQUIRED and its status message carries either a tool_approval_request or an ask_user_request. The client answers by sending a response message on the same task, and the agent resumes the turn where it left off.

Pause kinds

An agent pauses either to get permission before it acts or to ask a question. Each case raises its own request.

RequestRaised whenThe client answers with
tool_approval_requestThe agent wants to call a tool from a binding that sets requireApproval.tool_approval_response
ask_user_requestThe agent calls the built-in ask_user tool because it needs information only a person has.ask_user_response

Both use the same pause and resume mechanism, so a client that handles one can handle the other with a different payload.

The kagent and codex runtimes both raise ask_user_request. The claude runtime does not, because the upstream Claude Code tool that backed it was removed, so a claude agent pauses for tool approval only.

Require approval for a tool

An agent pauses for a tool only when its binding asks for that. Set requireApproval on an mcp tool binding in the AgentTemplate, and the agent stops before each call to a tool that the binding exposes.

apiVersion: kagent.dev/v1alpha3
kind: AgentTemplate
metadata:
  name: cluster-operator
  namespace: kagent
  labels:
    kagent.dev/harness: kagent
spec:
  tools:
    - mcp:
        server:
          kind: RemoteMCPServer
          name: kagent-tool-server
        tools:
          - k8s_delete_resource
          - k8s_patch_resource
        requireApproval: true
FieldDescription
mcp.toolsThe names of the tools to bind. Omit the list, or leave it empty, to bind every tool that the server offers.
mcp.requireApprovalPauses before each invocation of a tool that this binding exposes. The pause covers the tools in mcp.tools, or every tool on the server when mcp.tools is omitted or empty. Omit to run the bound tools without approval.

For the rest of the binding’s fields, see About tools.

Approval belongs to the binding rather than to the tool name, so one server can supply both kinds of tool. Bind the tools that need a person in a binding that sets requireApproval, and bind the rest in a second binding that omits it.

Note

kagent 0.x named the tools that needed approval in a requireApproval list on the Agent resource, which matched tool names across every server. In 1.x, approval is a property of one binding, so the same tool name can pause for one server and run freely for another.

Two limits apply to what a binding can express, and both depend on the runtime:

RuntimeApprovalSplitting one server across two bindings
kagentSupported.Supported.
codexSupported.Rejected, with RemoteMCPServer "<name>" is bound more than once.
claudeSupported.Rejected, with RemoteMCPServer "<name>" is bound more than once. A binding whose tool selection kagent cannot verify against the server’s discovered tools exposes the whole server and reports a warning.

Anything the binding does not cover runs without a pause. A built-in tool, such as file access, shell, or web search, and any MCP tool on a binding that omits requireApproval, is approved automatically. The sandbox is the boundary that contains those calls. For more information, see Sandboxing.

Negotiate the extension

HITL is an A2A message extension, identified by a versioned URI. A client requests it by setting that URI as the A2A-Extensions header on the call that sends a message.

A2A-Extensions: https://kagent.dev/extensions/hitl/v1

kagent activates the extension only for calls that request it, and echoes the activated URI back. A client that never requests the extension sees ordinary turns until the agent needs a person. The turn then pauses like any other, and that client has no way to answer the request.

A call from outside the cluster addresses the agent with two more headers, because the gateway routes on metadata rather than on a path. Port-forward the controller’s gRPC port first, as in Install kagent.

grpcurl -plaintext \
  -H 'A2A-Extensions: https://kagent.dev/extensions/hitl/v1' \
  -H 'x-kagent-agent-instance-id: <instance-id>' \
  -d '{
    "message": {
      "messageId": "msg-1",
      "role": "ROLE_USER",
      "parts": [{"text": "Delete the obsolete pod in the production namespace."}]
    }
  }' localhost:8083 lf.a2a.v1.A2AService/SendStreamingMessage

When the agent pauses, the payload arrives in the status message’s metadata, keyed by the extension URI. The URI is also listed in the message’s extensions array. Each payload carries a type field that specifies its shape.

TypeDirection
tool_approval_requestAgent to client
ask_user_requestAgent to client
tool_approval_responseClient to agent
ask_user_responseClient to agent

Warning

In case of failure, both halves of this negotiation fail silently, and neither failure reports anything.

  • A send that omits the header produces a pause that cannot be answered. The turn still stops, but its status message carries the question as prose, with no metadata and no correlation id, so there is nothing to render and no id to answer with. Re-reading that task with the header does not recover it, because the payload was never attached. Send the header on every call: it is harmless on a read, and unrecoverable if missed on a send. An attached payload is stored with the task, so a later read returns it whether or not that read requests the extension.
  • A response that omits the extensions array is delivered as ordinary text. kagent ignores the metadata payload unless the message itself lists the extension URI in extensions. The task resumes and the agent replies, so the call looks like it worked, but the structured decision never reached the agent.

Approving or rejecting a tool

A tool_approval_request lists the pending calls, each with an id, the tool name, and the args the agent chose. The response decides every listed call.

{
  "type": "tool_approval_response",
  "approvals": [
    { "id": "<tool-id>", "approved": true },
    { "id": "<other-tool-id>", "approved": false, "rejection_reason": "Deleting that namespace is out of scope." }
  ]
}

A response must decide every call in the request. A rejection reason is optional but worth sending, because the agent receives it and can adapt rather than simply failing.

Answering a question

An ask_user_request carries an id and a list of questions. The response echoes the same id and answers them in order.

{
  "type": "ask_user_response",
  "id": "<request-id>",
  "answers": [
    { "answer": ["us-east-1"] }
  ]
}

Resume a paused task

A paused task waits. To resume, the client sends a message on the same task and context, carrying the response payload. kagent rejects a resume attempt on a task that is not waiting, with task is not waiting for input.

While the task waits, kagent pauses the ActorActorThe sandboxed unit of compute, provided by Agent Substrate, that runs an AgentInstance's conversation loop. Every AgentInstance is backed by one.Learn more rather than suspending it. A pause keeps the running process in a full snapshotSnapshotThe stored state that an Actor suspends to, held in object storage. Resuming restores the Actor from its most recent snapshot, which is what makes suspending idle agents cheap.Learn more on node-local storage, so a runtime that holds a live process across the wait, such as codex or claude, continues the same turn on resume. The WorkerWorkerA pre-started, sandboxed pod that hosts at most one Actor at a time. Actors are multiplexed across a pool of Workers over time.Learn more is released in the meantime, so a conversation that sits at INPUT_REQUIRED costs no pool capacity. For more information on the suspend that a finished turn uses instead, see Suspend and resume.

Because the transcriptTranscriptThe record of an AgentInstance's conversation, held server-side and append-only. It survives the Actor suspending between turns, and a resumed runtime cannot shrink it. only grows, the question and the answer both stay in the task history, so a later reader can see what was asked and what a person decided.

Task states

An A2A task moves through several states over its life. Two of them mean that the task has stopped and is waiting on a person, rather than working.

StateMeaning
INPUT_REQUIREDThe agent is waiting for a person. This is the state that a tool approval or a question produces.
AUTH_REQUIREDThe agent is waiting for credentials. kagent’s own runtimes never set this state, but its gateway accepts a resume from it, so a byo runtime that produces it works.

Agents bound as tools

An agent that a parent binds as a tool can raise a pause of its own. The request then carries a nested block naming the subagent, along with its task and context, so a client can tell the person which agent is actually asking rather than attributing it to the parent.

Client support

The AgentTemplate decides that a turn pauses, but the client decides whether a person can answer it. What someone can do with a pause therefore depends on which client raised the turn.

ClientHITL
Your own A2A clientFull. Request the extension URI and handle the four payload types.
An MCPMCPModel Context Protocol, an open protocol for exposing tools and resources to a model. kagent reaches an MCP server through a RemoteMCPServer resource, and an AgentTemplate binds individual tools from it.Learn more client that supports tasksSupported. invoke_agent_instance returns a task, and input requests surface as MCP elicitations.
The kagent CLINot supported, and a turn that pauses is stranded. kagent invoke does not request the extension, so an agent that needs a person parks the task at INPUT_REQUIRED with nothing to answer it by. The CLI reports Input required to continue this AgentInstance. and stops there. Send the turn again from a client that requests the extension.

Next steps