n4nAI

UserProxyAgent human_input_mode explained with examples

Explains autogen userproxyagent human_input_mode: the ALWAYS, TERMINATE, NEVER settings, how the conversation loop uses them, with runnable examples.

n4n Team4 min read789 words

Audio narration

Coming soon — every post will get a voice note here.

The autogen userproxyagent human_input_mode parameter controls when the UserProxyAgent pauses a multi-agent conversation to solicit input from a human operator. It is a string enum accepting three values—"ALWAYS", "TERMINATE", and "NEVER"—and it sits at the core of AutoGen’s human-in-the-loop (HITL) design.

What autogen.userproxyagent.human_input_mode Actually Is

In AutoGen’s agent chat framework, UserProxyAgent is the bridge between autonomous agents and a physical user. The human_input_mode attribute dictates the cadence of human intervention. Set to "ALWAYS", the proxy blocks on every turn and waits for keyboard input before it forwards anything to the next agent. Set to "TERMINATE", it stays silent during normal operation but prompts the user when the conversation is about to end—either because a termination message was detected or the max_consecutive_auto_reply limit was hit. Set to "NEVER", it never prompts; the agent runs fully unattended (subject to code-execution guards).

This is not a timeout or a retry policy. It is a deterministic branch in the agent’s generate_reply logic. Understanding it prevents you from shipping a “human-in-the-loop” system that either nags the operator 50 times per task or silently executes destructive code.

from autogen import UserProxyAgent, AssistantAgent

proxy_always = UserProxyAgent(
    name="admin",
    human_input_mode="ALWAYS",
    code_execution_config={"use_docker": False},
)

proxy_terminate = UserProxyAgent(
    name="admin",
    human_input_mode="TERMINATE",
    code_execution_config={"use_docker": False},
)

proxy_never = UserProxyAgent(
    name="admin",
    human_input_mode="NEVER",
    code_execution_config={"use_docker": False},
)

How It Works Under the Hood

AutoGen drives a conversation as a sequence of message passes between agents. When an agent calls send to the UserProxyAgent, the proxy’s generate_reply runs. The simplified decision tree looks like this:

def generate_reply(self, messages, sender):
    if self.human_input_mode == "ALWAYS":
        human_msg = self.get_human_input("ALWAYS mode, type your input:")
        if human_msg:
            return human_msg
    if self._is_termination_msg(messages[-1]) or self._exceeded_auto_reply(sender):
        if self.human_input_mode in ("TERMINATE", "ALWAYS"):
            human_msg = self.get_human_input("Terminate? [y/N]:")
            if human_msg and self._should_terminate(human_msg):
                return None  # signal termination
    # otherwise perform auto reply (code exec, echo, etc.)
    return self._auto_reply(messages, sender)

Key point: human_input_mode does not affect whether the proxy can execute code or call the LLM. It only gates the get_human_input call. If you set "NEVER" but leave code_execution_config enabled, the proxy will still run Python locally—potentially dangerous if the assistant is compromised.

The get_human_input method reads from sys.stdin in CLI deployments, or from a registered callback in web deployments. In practice, you override it to integrate with Slack, a web form, or a queue.

Why human_input_mode Matters for HITL

Human-in-the-loop is not a checkbox. It is a liability and a safety boundary. The autogen userproxyagent human_input_mode is the primary knob for balancing autonomy against control:

  • ALWAYS is for debugging or high-risk workflows where every agent step must be eyeballed. Expect high operator fatigue.
  • TERMINATE is the default and usually correct choice for semi-autonomous tasks: the model works, and the human approves the final artifact or cancellation.
  • NEVER is for batch jobs, simulations, or inner-loop agents that are themselves supervised by an outer TERMINATE proxy.

If you are building a customer-facing copilot, shipping with "NEVER" because it “worked in the demo” is how you get a viral screenshot of your app deleting a database. Conversely, "ALWAYS" in a latency-sensitive pipeline will crater throughput because a human is now the critical path.

Concrete Example: Three Modes Side by Side

Below is a minimal script that runs the same task under each mode. We use a dummy assistant that replies with a fixed string to keep output predictable.

from autogen import UserProxyAgent, AssistantAgent

def make_assistant():
    return AssistantAgent(
        name="assistant",
        llm_config={"config_list": [{"model": "gpt-4o-mini", "api_key": "sk-..."}]},
        system_message="Reply with 'done' only.",
    )

def run_mode(mode):
    proxy = UserProxyAgent(
        name="human",
        human_input_mode=mode,
        code_execution_config=False,
        max_consecutive_auto_reply=2,
    )
    assistant = make_assistant()
    proxy.initiate_chat(assistant, message="Start")
    print(f"--- mode={mode} finished ---\n")

# Simulate ALWAYS with piped empty inputs
import sys
from io import StringIO

for mode in ["ALWAYS", "TERMINATE", "NEVER"]:
    # In a real terminal, ALWAYS would block for input; here we fake it.
    sys.stdin = StringIO("\n\n")
    run_mode(mode)

Terminal behavior contrast:

  • ALWAYS: prints prompt before each assistant turn. Operator pressing Enter accepts the auto-reply (empty input means “no extra instruction”).
  • TERMINATE: no prompts until the assistant says a termination phrase or max_consecutive_auto_reply hits. Then one prompt appears.
  • NEVER: zero prompts; the chat runs to completion and exits.

A more realistic deployment overrides get_human_input:

class SlackUserProxy(UserProxyAgent):
    def get_human_input(self, prompt):
        return slack_client.wait_for_reply(channel="C123", prompt=prompt)

This keeps the same human_input_mode semantics while moving the human off the terminal.

Common Misconceptions

“ALWAYS means the human must type something.”
No. An empty string (just hitting Enter) is valid input and the proxy proceeds with its default auto-reply. Many production wrappers treat empty input as “continue”.

“NEVER removes the human from the loop entirely.”
Only from the conversation loop. If code_execution_config is active, the proxy may still execute code without asking. To truly sandbox, set code_execution_config=False or route execution to a container with no privileges.

“TERMINATE is just ALWAYS at the end.”
Not equivalent. In ALWAYS, the human can inject new instructions mid-task, changing direction. In TERMINATE, they only see the prompt when the agent thinks it’s done; they cannot steer intermediate steps unless they terminate and restart.

“human_input_mode is per-conversation.”
It is set at agent construction and is fixed for that agent instance. You can create multiple proxies with different modes and swap them, but you cannot flip it dynamically without subclassing.

Production Tips

When you deploy AutoGen outside a notebook, wrap UserProxyAgent in a service that maps get_human_input to your UI. Keep max_consecutive_auto_reply low (1–3) for TERMINATE modes to avoid runaway loops. Log every human input and the preceding agent message; this audit trail is your only evidence if the agent misbehaves.

If you point AutoGen at an OpenAI-compatible gateway such as n4n.ai, the autogen userproxyagent human_input_mode logic remains entirely client-side—the gateway just returns model completions, and fallback or cache hints do not alter when your proxy decides to prompt a human.

Finally, test each mode with a mock human (like the StringIO trick above) in CI. A regression that flips TERMINATE to NEVER can go unnoticed until prod runs unattended and emails your CEO.

Tagsautogenuserproxyagenthuman-in-the-looptutorial

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All autogen human-in-the-loop workflows posts →