mcpField
A
ag2-hitl

ag2-hitl

github.com/ag2aiAgent workflows1268 installsUpdated 13w ago
Repository

About ag2-hitl

A skill for ag2 developers adding human oversight to agents. It explains two mechanisms: collecting input during a tool run and requiring approval before a specific tool executes.

Key features

  1. 01Collect input with context.input()
  2. 02Receive responses through hitl_hook
  3. 03Gate tool calls with middleware
  4. 04Customize prompts and denial messages
  5. 05Handle timeouts and denials

Use cases

  1. 01Request confirmation before risky actions
  2. 02Collect credentials or context mid-conversation
  3. 03Get human review before finalizing work

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/ag2ai/ag2-skills --skill ag2-hitl

After installing, type/ag2-hitlin Claude Code to confirm it loaded.

Details

From the project README

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:

NeedUse
Tool asks an open question and waits for a typed answercontext.input() from inside the tool + hitl_hook on the agent
Approve / deny a specific tool call before its body runsapproval_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.

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:

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.

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

@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.

Comments

Markdown supported