Understanding Human-in-the-Loop in Agent Systems
Implementing Human-in-the-Loop for AgentScope Workflows is one of the most practical skills you can develop as an AI agent developer. Fully autonomous agents are powerful, but production systems almost always require a mechanism for a human to review, redirect, or halt an agent mid-task. Without this safety valve, a runaway agent can take irreversible actions — deleting files, sending emails, or making API calls — before you have a chance to intervene.
Human-in-the-Loop (HITL) is the design pattern where a human operator can observe an agent’s intermediate outputs and inject corrections, approvals, or new instructions before the agent proceeds. This is distinct from simply reviewing the final output; HITL is about controlling the agent during execution.
In multi-agent frameworks like AgentScope, HITL becomes even more nuanced. You’re not just pausing one agent — you’re potentially pausing an entire pipeline of cooperating agents. Understanding how AgentScope structures this interaction is the key to building reliable, production-grade workflows.
Core Concepts of AgentScope’s HITL Mechanisms
AgentScope provides a clean, composable set of primitives for human intervention. Before writing any code, you need to understand the four building blocks.
ReActAgent
The ReActAgent is the primary agent class for tasks that involve reasoning and tool use. It follows the ReAct (Reason + Act) pattern: the agent reasons about what to do, selects a tool, observes the result, and iterates. Crucially, ReActAgent is designed to be interruptible. Its execution can be paused at any point in the reasoning loop to accept new input.
If you’ve used older AgentScope versions, note that DialogAgent and DictDialogAgent are now deprecated. All new code should use ReActAgent.
UserAgent
The UserAgent is the counterpart to ReActAgent in a HITL workflow. It represents the human operator. When called, it blocks execution and prompts the terminal (or your UI layer) for input. This input is then packaged and returned as a message that the ReActAgent can consume.
Msg
The Msg object is the universal message format in AgentScope. Every piece of data flowing between agents — whether it’s an agent’s reasoning trace, a tool result, or human text — is wrapped in a Msg. When the UserAgent captures human input, it returns it as a Msg, allowing it to slot seamlessly into the existing conversation flow.
The Async Conversation Loop
As of AgentScope v1.0.0, the entire framework is asynchronous by default. This is a breaking change from older versions. All agent calls must be await-ed inside an async function. The conversation loop is an async while loop that alternates control between the ReActAgent and the UserAgent, creating the turn-by-turn HITL interaction.
Step-by-Step: Building an Interactive AgentScope Workflow
Let’s build a complete HITL workflow from scratch. The scenario: an agent that helps a developer review and refine a code snippet, with the developer able to provide feedback at each step.
Step 1: Install AgentScope
pip install agentscope
AgentScope requires Python 3.10 or higher. Verify your installation:
python -c "import agentscope; print(agentscope.__version__)"
Step 2: Configure Your API Key
The ReActAgent needs an LLM backend. For this example, we’ll use Dashscope (Qwen). Export your key before running:
export DASHSCOPE_API_KEY="your-api-key-here"
Step 3: Build the HITL Workflow
Here is a complete, runnable example:
import asyncio
import os
from agentscope.agent import ReActAgent, UserAgent
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
async def main():
# 1. Instantiate your LLM model object
model = DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
)
# 2. Instantiate the UserAgent (represents the human operator)
user = UserAgent(name="User")
# 3. Instantiate the ReActAgent with the model
agent = ReActAgent(
name="CodeReviewer",
model=model,
sys_prompt=(
"You are a senior Python developer. "
"Review the code the user provides and suggest improvements. "
"Wait for user feedback before proceeding."
),
)
# 4. Seed the conversation with an initial task
initial_msg = Msg(
name="User",
content="Please review this function:\n\ndef add(a, b):\n return a+b",
role="user",
)
msg = initial_msg
# 5. The HITL conversation loop
while True:
# Agent processes the current message
msg = await agent(msg)
# Print the agent's output for visibility
print(f"\n[Agent]: {msg.content}\n")
# UserAgent prompts for human input
msg = await user(msg)
# Exit condition: user types "exit"
if msg.content.strip().lower() == "exit":
print("Session ended by user.")
break
asyncio.run(main())
How the Loop Works
- The initial
Msgcarrying the code snippet is passed to theReActAgent. - The agent reasons about the code and returns a review as a new
Msg. - The
UserAgentpauses execution and prompts the terminal: “User: ”. - The human types feedback (e.g., “Focus on type hints”) and presses Enter.
- The
UserAgentwraps that text in aMsgand returns it. - The loop sends this new
Msgback to theReActAgent, which incorporates the feedback. - This continues until the user types “exit”.
This loop is the foundation of every HITL pattern in AgentScope.
Advanced HITL Patterns and Customizations
Once you’ve mastered the basic loop, there are several advanced patterns worth knowing.
Conditional Interruption
You may not want to pause for human input on every turn. A common pattern is to only interrupt when the agent signals uncertainty or requests approval:
async def main():
model = DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
)
user = UserAgent(name="User")
agent = ReActAgent(
name="TaskAgent",
model=model,
sys_prompt=(
"When you are unsure, say 'NEEDS_APPROVAL:' at the start "
"of your response. Otherwise, continue autonomously."
),
)
msg = Msg(name="User", content="Refactor the database module.", role="user")
while True:
msg = await agent(msg)
# Only interrupt if agent signals it needs approval
if "NEEDS_APPROVAL:" in msg.content:
print(f"\n[Agent needs approval]: {msg.content}\n")
msg = await user(msg)
else:
print(f"\n[Agent proceeding autonomously]: {msg.content}\n")
# No user input needed — loop continues
# Add your own exit condition here
break
asyncio.run(main())
This pattern lets the agent run freely for routine tasks while surfacing only genuinely ambiguous decisions to the human.
Timeout-Based Interruption
For production systems, you may want the agent to proceed with a default action if the human doesn’t respond within a time window:
async def get_user_input_with_timeout(user_agent, msg, timeout_seconds=30):
"""Await user input with a fallback timeout."""
try:
return await asyncio.wait_for(user_agent(msg), timeout=timeout_seconds)
except asyncio.TimeoutError:
print(f"\n[Timeout] No input received after {timeout_seconds}s. Proceeding with defaults.\n")
# Return a default approval message
return Msg(name="User", content="approved", role="user")
This approach uses asyncio’s native cancellation support — the “realtime interruption via cancellation” mentioned in AgentScope’s documentation.
Multi-Step Approval Gates
For workflows with multiple phases, you can define explicit checkpoints where human approval is mandatory:
async def run_with_checkpoints(agent, user, phases):
"""Run an agent through multiple phases with mandatory human approval gates."""
msg = None
for phase_prompt in phases:
# Start each phase with the phase prompt
msg = Msg(name="User", content=phase_prompt, role="user")
msg = await agent(msg)
print(f"\n[Phase complete. Agent output]: {msg.content}")
print("Type 'approve' to continue or 'stop' to halt.\n")
approval = await user(msg)
if approval.content.strip().lower() == "stop":
print("Workflow halted by user.")
return
print("All phases complete.")
phases = [
"Phase 1: Analyze the requirements document.",
"Phase 2: Draft the implementation plan.",
"Phase 3: Write the unit tests.",
]
asyncio.run(run_with_checkpoints(agent, user, phases))
Real-World Use Cases and Best Practices
Use Case 1: Code Review Assistant
A ReActAgent with access to a code analysis tool reviews a pull request. At each flagged issue, the human decides whether to accept the suggestion, modify it, or skip it. This mirrors the collaborative review process developers already know.
Use Case 2: Content Generation Pipeline
An agent drafts marketing copy, blog outlines, or documentation. The human reviews each draft and provides direction — “make it shorter,” “add a CTA,” “use a more formal tone” — before the next draft is generated. This is significantly faster than writing from scratch while maintaining human editorial control.
Use Case 3: Automated Data Processing with Anomaly Review
An agent processes rows in a dataset autonomously. When it encounters an anomaly (missing value, unexpected format), it pauses and presents the row to a human analyst for manual classification before continuing. This is a powerful pattern for data pipelines that can’t afford silent errors.
For teams building similar decision-support agents, frameworks like AgentScope complement RAG-based knowledge retrieval well — see What Is RAG? Retrieval-Augmented Generation Explained for how to combine these approaches. You can also explore how role-based agent designs handle human oversight in gstack Gears and Personas: Role-Based AI Development.
Best Practices
- Keep human prompts specific. When the
UserAgentprompts for input, print the agent’s full reasoning trace so the human has enough context to give useful feedback. - Log every interaction. Persist each
Msgto a database or log file. In production, you need an audit trail of every human decision. - Design explicit exit conditions. Never rely on the user knowing to type “exit.” Build structured options (“approve / reject / modify”) into your prompt interface.
- Test with simulated human input. For automated testing, replace
UserAgentwith a function that returns pre-scriptedMsgobjects. This lets you test your HITL logic without manual intervention. - Limit agent autonomy in sensitive domains. For actions involving money, external communications, or irreversible system changes, require explicit human approval regardless of agent confidence.
Frequently Asked Questions
Can I use AgentScope’s HITL pattern with agents that use tools?
Yes. The ReActAgent supports both tool use and human interruption simultaneously. You can define tools in the agent’s constructor and the HITL conversation loop works unchanged. The agent will call tools autonomously as part of its reasoning, but the loop still pauses for human input between each top-level turn.
What happens if I call an agent synchronously instead of with await?
You’ll get a runtime error. Since AgentScope v1.0.0, all agent calls are asynchronous and must be awaited inside an async function. If you’re adapting old synchronous code, you’ll need to wrap everything in async def functions and use asyncio.run() as the entry point.
How do I integrate the UserAgent with a web UI instead of the terminal?
The UserAgent by default reads from stdin. For a web UI, you would replace the UserAgent with a custom agent class that reads from a queue, a WebSocket, or an API endpoint instead. The rest of the conversation loop remains identical — your custom agent still returns a Msg object.
Can the human completely take over from the agent mid-task?
Yes. Because the human input is returned as a Msg that the ReActAgent receives as its next input, you can have the human provide entirely new instructions that redirect the agent. The agent will treat these as new directives and adjust its behavior accordingly. There’s no special “takeover” API — the conversation loop handles it naturally.
Is it possible to have multiple human operators in one workflow?
AgentScope doesn’t natively route to multiple UserAgent instances in one loop, but you can instantiate multiple UserAgent objects and call them conditionally within your loop — for example, routing technical decisions to a developer’s terminal and business decisions to a manager’s terminal. Each still wraps its input in a Msg that the ReActAgent can consume.