# Ag2 Hitl

> Pause an AG2 `Agent` mid-run to collect human input via `context.input()`, or gate a tool call with `approval_required()` middleware. Use when the user wants the agent to ask for confirmation, request missing info (passwords, API keys, data), or have a human approve sensitive / irreversible / expensive tool calls (sending emails, deleting records, payments).

- Skill: `ag2ai/ag2-hitl` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ag2ai/ag2-hitl`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ag2ai/ag2-hitl/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: Apache-2.0
- Author: ag2ai (https://skillmd.com/u/ag2ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ag2ai/ag2-hitl

---


# Human-in-the-loop

## When to use

- The agent should **ask for confirmation** before doing something risky.
- The agent needs **information from the user mid-conversation** (a password, an API key, missing context).
- A specific **tool call should require human approval** before it runs (irreversible / expensive / sensitive).
- Quality assurance — show a draft, get human edits/approval before finalising.

Two distinct mechanisms — pick by intent:

| Need | Use |
|---|---|
| Tool asks an open question and waits for a typed answer | `context.input()` from inside the tool + `hitl_hook` on the agent |
| Approve / deny a specific tool call before its body runs | `approval_required()` tool middleware |

## Pattern 1 — `context.input()` for open questions

A tool requests input via `Context.input(message, timeout=...)`. The agent must have a `hitl_hook` that knows how to collect that input.

```python
from ag2 import Agent, Context, tool
from ag2.events import HumanInputRequest, HumanMessage

@tool
async def execute_query(context: Context) -> str:
    answer = await context.input(
        "Are you sure you want to run this query? (yes/no)",
        timeout=60.0,
    )
    if answer.strip().lower() != "yes":
        return "Query cancelled."
    return "Query executed successfully."

def hitl_hook(event: HumanInputRequest) -> HumanMessage:
    print(f"Agent asks: {event.content}")
    return HumanMessage(content=input("Your answer: "))

agent = Agent("dba", tools=[execute_query], hitl_hook=hitl_hook)
```

The hook receives a `HumanInputRequest` (the prompt is in `event.content`) and returns either a `HumanMessage` or a plain `str` (the framework wraps a `str` via `HumanMessage.ensure_message`). Both `def` and `async def` hooks are supported.

You can also register the hook after construction:

```python
agent = Agent("dba", tools=[execute_query])

@agent.hitl_hook
async def async_hitl_hook(event: HumanInputRequest) -> HumanMessage:
    answer = await collect_from_ui(event.content)
    return HumanMessage(content=answer)
```

The decorator overrides any hook set in the constructor — but if one was already set (e.g. via the constructor), applying `@agent.hitl_hook` emits a `RuntimeWarning` (`"You already set HITL hook, provided value overrides it"`). Set the hook in exactly one place to avoid the warning.

The hook participates in dependency injection — `Context`, `Inject`, `Variable`, `Depends` work the same as in tools.

If `context.input()` is called and no hook is registered, the framework raises `HumanInputNotProvidedError`.

## Pattern 2 — `approval_required()` for specific tool calls

Gate a single tool with the built-in approval middleware. The user is prompted before the tool body runs and can approve or deny.

```python
from ag2 import Agent, tool
from ag2.config import OpenAIConfig
from ag2.middleware import approval_required

@tool(middleware=[approval_required()])
def delete_account(user_id: str) -> str:
    """Deletes a user account by ID permanently."""
    return f"Account {user_id} deleted."

agent = Agent(
    "support",
    config=OpenAIConfig(model="gpt-4o-mini"),
    tools=[delete_account],
    hitl_hook=lambda event: input(event.content),
)
```

When the agent calls `delete_account`, the user sees (with the default `allow_always=True`):

```
Agent wants to call the tool:
`delete_account`, {"user_id": "abc-123"}
Please approve or deny this request.
Y/N/Always?
```

The answer is lowercased before matching. `y`, `yes`, or `1` approve this one call; `always` approves this call and all subsequent calls of the same tool in the same context (it sets a per-context bypass flag). Anything else denies it; the agent receives `denied_message` (default `"User denied the tool call request"`) and can adjust. The default `timeout` is **30** seconds. Set `allow_always=False` to drop the "Always" option (the prompt then shows just `Y/N?`).

`approval_required()` calls `context.input()` under the hood, so it **also requires a `hitl_hook`** — without one you'll get a runtime error.

### Custom prompt

```python
@tool(middleware=[approval_required(
    message="⚠️ Run `{tool_name}` with {tool_arguments}? (y/n)",
    denied_message="Operation blocked by user.",
)])
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email to the given address."""
    ...
```

`{tool_name}` and `{tool_arguments}` are interpolated.

## Pairing both patterns

For a tool that both gathers input mid-run and requires approval:

```python
@tool(middleware=[approval_required()])
async def schedule_report(name: str, context: Context) -> str:
    """Schedule a report — asks the user for the cadence, then runs after approval."""
    cadence = await context.input("How often? (daily / weekly / monthly)")
    return f"Scheduled '{name}' on {cadence} cadence."
```

The approval middleware runs first (outermost). Once approved, the tool body executes and `context.input()` triggers a second human interaction.

## Going deeper

- Source docs: `website/docs/user-guide/context/human_in_the_loop.mdx` (`context.input`, `hitl_hook`), `website/docs/user-guide/tools/approval_required.mdx` (`approval_required` middleware).
- Tool middleware in general — `website/docs/user-guide/tools/tool_middleware.mdx`. See also `ag2-middleware` for agent-wide HITL interception via `BaseMiddleware.on_human_input()`.
- HITL hooks support dependency injection identically to tools — see `../ag2-add-custom-tool/references/dependency_injection.md`.

## Common pitfalls

- **`approval_required()` without a `hitl_hook`** — the middleware calls `context.input()`, so the agent needs a hook. You'll see `HumanInputNotProvidedError` otherwise.
- **Forgetting to handle the denial path** — `context.input()` returns whatever the hook returns. If you only branch on "yes", any other answer (including silence/default) lets the operation continue. Always validate.
- **Sync `input()` in an async UI** — `input()` blocks the event loop. Use an async hook (`async def`) and an async input collector (web socket, message queue) for any non-CLI app.
- **No timeout** — `context.input(prompt)` can wait forever. Pass `timeout=60.0` (seconds) for any production path.
- **Decorator hook overrides constructor hook** — if you set both, the decorator wins and emits a `RuntimeWarning`. Pick one place.
- **Expecting `HumanMessage` to flow into the conversation history automatically** — it does for the requesting tool's return value, but mid-run inputs collected via `ctx.input()` are not separate user turns. They live in the tool's scope.

