Human-in-the-Loop (HIL)
The Human-in-the-Loop system enables agents to pause execution and request human approval, input, or review before proceeding with sensitive operations.
Overview
HIL provides:
- Durable execution - Survives process restarts via LangGraph checkpointing
- Async responses - Can wait hours or days for human input
- Multiple interaction types - Authorization, input collection, review
- MCP tool integration -
ask_humanandrequest_authorizationtools - Security levels - LOW, MEDIUM, HIGH, CRITICAL risk classification
- Schema validation - Validate user input with JSON schemas and retry support
Comparison with Claude SDK
| Feature | Claude SDK | isA Agent SDK | Advantage |
|---|---|---|---|
| Basic Input | human_input() | collect_input() | Equivalent |
| Selection | N/A | collect_selection() | More powerful |
| Authorization | Hooks | request_authorization() | More powerful |
| Review/Edit | N/A | request_review() | More powerful |
| Combined Input+Auth | N/A | request_input_with_authorization() | More powerful |
| Durable Execution | No | Yes (LangGraph checkpoints) | More powerful |
| MCP Integration | No | Yes (ask_human tool) | More powerful |
| Schema Validation | No | Yes (with retry) | More powerful |
| Security Levels | No | Yes (LOW→CRITICAL) | More powerful |
The isA HIL system is a superset of Claude SDK’s human_input() functionality, providing additional methods for structured input, authorization workflows, and durable execution.
Basic Authorization
Request Tool Permission
Pause execution until human approves a tool use:
from isa_agent_sdk import request_tool_permission
# In your agent logic
authorized = await request_tool_permission(
tool_name="delete_file",
tool_args={"path": "/important/data.txt"},
reason="Need to clean up old data files",
security_level="high"
)
if authorized:
# Proceed with deletion
await delete_file("/important/data.txt")
else:
# Handle rejection
print("User declined the operation")Generic Authorization
For any action requiring approval:
from isa_agent_sdk import request_authorization
authorized = await request_authorization(
action="Deploy to production",
reason="All tests passed, ready for release",
context={
"version": "2.1.0",
"changes": 15,
"tests_passed": 142
},
risk_level="high"
)Input Collection
Collect Text Input
from isa_agent_sdk import collect_input
# Simple text input
api_key = await collect_input(
prompt="Please enter your API key",
description="Required for authentication",
input_type="text"
)
# Number input
count = await collect_input(
prompt="How many items to process?",
input_type="number"
)
# Boolean input
confirm = await collect_input(
prompt="Enable debug mode?",
input_type="boolean"
)Collect with Schema Validation
config = await collect_input(
prompt="Enter configuration",
input_type="object",
schema={
"type": "object",
"properties": {
"name": {"type": "string"},
"count": {"type": "integer", "minimum": 1},
"enabled": {"type": "boolean"}
},
"required": ["name", "count"]
}
)Collect Credentials
Secure credential collection:
from isa_agent_sdk import collect_credentials
# API key
api_key = await collect_credentials(
prompt="Enter your OpenAI API key",
credential_type="api_key",
description="Required for LLM access"
)
# Password
password = await collect_credentials(
prompt="Enter database password",
credential_type="password"
)
# Token
token = await collect_credentials(
prompt="Enter GitHub token",
credential_type="token"
)Collect Selection
Choose from predefined options:
from isa_agent_sdk import collect_selection
choice = await collect_selection(
prompt="Select deployment environment",
options=["development", "staging", "production"],
description="Choose where to deploy"
)
print(f"Deploying to: {choice}")Review and Approval
Request Content Review
Ask human to review generated content:
from isa_agent_sdk import request_review
# Review code
result = await request_review(
content=generated_code,
content_type="code",
instructions="Please review this code for security issues",
editable=True # Allow user to edit
)
if result["approved"]:
final_code = result.get("edited_content", generated_code)
await save_code(final_code)
else:
action = result.get("action", "rejected")
print(f"Review result: {action}")Content Types:
text- General text contentcode- Source codeplan- Execution planconfig- Configuration data
Request Plan Approval
Specialized for execution plans:
from isa_agent_sdk import request_plan_approval
plan = """
## Deployment Plan
1. Run database migrations
2. Deploy backend services
3. Deploy frontend
4. Run smoke tests
5. Switch traffic to new version
"""
result = await request_plan_approval(
plan=plan,
plan_title="Production Deployment v2.1.0"
)
if result["approved"]:
await execute_deployment()Plan-Review Card (Chat Product UX)
Inside isA_Mate and the web console, a request_plan_approval call doesn’t just sit as a raw interrupt — it renders as a plan-review card in the chat UI.
How it renders:
-
The review-HIL handler emits a
hil_requestcustom stream event before interrupting the graph (SDK ≥0.3.33), instead of leaving the tool call stuck at “Running” with no client-visible state. -
isA_Mate’s stream processor forwards this as an
hil.requestSSE event:{ "type": "hil.request", "session_id": "...", "content": "...", "metadata": { "request_id": "...", "request_type": "approval", "interrupt_data": { "content": { "tasks": [...], "execution_mode": "...", "total_tasks": 3 } } } } -
The frontend extracts the plan from
metadata.interrupt_data(tolerating a couple of historical nesting shapes) and renders it as “Review Execution Plan” — a numbered task list, plusexecution_modeandsolution_hypothesiswhen the plan carries them.
Responding:
- Approve — optionally edit the plan first; the card sends back
{ approved: true, action: "approve", execute_on_approve: true, plan_id, edited_content? }. - Reject — sends
{ approved: false, decision: "rejected", reason }.
Resume behavior:
- Approving auto-executes: since SDK 0.3.34, an approved plan runs task-by-task with per-task
task.*progress events. (Prior to 0.3.34, approving a plan re-narrated it and waited for the user to say “proceed” instead of running it — that was a bug, fixed in isA_Agent_SDK#1018.) - Since SDK 0.3.48,
resume()refuses to resume a thread with nothing pending (no_pending_interrupt) instead of silently replaying stale checkpoint content — this closes a class of bug where an approved plan appeared to do nothing on a stale or shared thread.
See the MCP HIL reference for the underlying tool-level event contract.
Request Execution Choice
Let user choose between options:
from isa_agent_sdk import request_execution_choice
choice = await request_execution_choice(
prompt="How should I handle the failing tests?",
options=[
{"id": "fix", "label": "Fix the tests", "description": "I'll update the test code"},
{"id": "skip", "label": "Skip for now", "description": "Continue without these tests"},
{"id": "abort", "label": "Abort", "description": "Stop the current operation"}
],
recommendation="fix" # Highlight recommended option
)
match choice:
case "fix":
await fix_tests()
case "skip":
await continue_without_tests()
case "abort":
raise AbortException("User aborted")Handling HIL in Streaming
Detecting HIL Requests
from isa_agent_sdk import query
async for msg in query("Delete old log files"):
if msg.is_hil_request or msg.is_checkpoint:
# HIL request detected
question = msg.metadata.get("question")
options = msg.metadata.get("options")
hil_type = msg.metadata.get("hil_type")
print(f"Approval needed: {question}")
# Respond to the checkpoint
await msg.respond({"authorized": True})
elif msg.is_text:
print(msg.content, end="")Resume After HIL
If the process restarts during HIL wait:
from isa_agent_sdk import resume
# Resume with user's response
async for msg in resume(
session_id="my-session-123",
resume_value={"authorized": True, "input": "user provided data"}
):
print(msg.content, end="" if msg.is_text else "\n")HIL Statistics
Get Interrupt Stats
from isa_agent_sdk import get_hil_stats
stats = get_hil_stats()
print(f"Total interrupts: {stats.total}")
print(f"By type: {stats.by_type}")
print(f"By node: {stats.by_node}")
print(f"Latest: {stats.latest}")Note: get_hil_stats() returns an InterruptStats dataclass, not a dictionary.
Clear HIL History
from isa_agent_sdk import clear_hil_history
clear_hil_history() # For testing/cleanupIntegration Example
Complete HIL Workflow
from isa_agent_sdk import (
query,
request_tool_permission,
request_review,
collect_input,
ISAAgentOptions,
ExecutionMode
)
async def safe_file_operation():
options = ISAAgentOptions(
execution_mode=ExecutionMode.COLLABORATIVE,
allowed_tools=["read_file", "write_file", "delete_file"]
)
async for msg in query("Clean up the temp directory", options=options):
if msg.is_checkpoint:
hil_type = msg.metadata.get("hil_type")
if hil_type == "tool_permission":
# Ask for tool permission
tool = msg.metadata.get("tool_name")
args = msg.metadata.get("tool_args")
print(f"\nPermission requested for: {tool}")
print(f"Arguments: {args}")
user_input = input("Approve? (y/n): ")
await msg.respond({"authorized": user_input.lower() == "y"})
elif hil_type == "review":
# Show content for review
content = msg.metadata.get("content")
print(f"\nReview requested:\n{content}")
user_input = input("Approve? (y/n): ")
await msg.respond({"approved": user_input.lower() == "y"})
elif msg.is_text:
print(msg.content, end="")
elif msg.is_error:
print(f"\nError: {msg.content}")Web Application Integration
from fastapi import FastAPI, WebSocket
from isa_agent_sdk import query, resume
import json
app = FastAPI()
# Store pending HIL requests
pending_hil = {}
@app.websocket("/chat/{session_id}")
async def chat(websocket: WebSocket, session_id: str):
await websocket.accept()
async for msg in query("User prompt here", options={"session_id": session_id}):
if msg.is_checkpoint:
# Store HIL request and notify client
pending_hil[session_id] = msg
await websocket.send_json({
"type": "hil_request",
"question": msg.metadata.get("question"),
"options": msg.metadata.get("options"),
"hil_type": msg.metadata.get("hil_type")
})
return # Wait for response via separate endpoint
await websocket.send_json({
"type": msg.type,
"content": msg.content
})
@app.post("/hil/{session_id}/respond")
async def respond_hil(session_id: str, response: dict):
if session_id in pending_hil:
msg = pending_hil.pop(session_id)
await msg.respond(response)
return {"status": "ok"}
return {"status": "not_found"}MCP Tool Integration
The HIL system exposes tools via MCP (Model Context Protocol) that agents can call directly:
ask_human Tool
The ask_human MCP tool allows agents to request human input during execution:
# Tool definition in MCP
@mcp.tool()
async def ask_human(question: str, context: str = "", user_id: str = "default") -> str:
"""Ask the human for additional information or clarification"""
# Returns structured response with:
# - status: "human_input_requested"
# - action: "ask_human"
# - data: { question, context, user_id, instruction }When agents call this:
{
"status": "human_input_requested",
"action": "ask_human",
"data": {
"question": "What deployment environment should I use?",
"context": "Deploying application v2.1.0",
"instruction": "This request requires human input. The client should handle the interaction."
}
}request_authorization Tool
The request_authorization MCP tool handles permission requests for sensitive operations:
# Tool definition in MCP
@mcp.tool()
async def request_authorization(
tool_name: str,
reason: str,
user_id: str = "default",
tool_args: dict | None = None
) -> str:
"""Request human authorization before executing a tool"""
# Automatically determines security level based on tool name:
# - "delete", "forget" → HIGH
# - "admin" → CRITICAL
# - Default → MEDIUMResponse includes:
request_id- Unique authorization request IDsecurity_level- AUTO-determined: MEDIUM, HIGH, or CRITICALexpires_at- Authorization expiration timestamp
Direct HIL Service Access
For advanced use cases, access the HIL service directly:
from isa_agent_sdk.services.human_in_the_loop import get_hil_service
hil_service = get_hil_service()
# Ask with interrupt (pauses graph execution)
response = hil_service.ask_human_with_interrupt(
question="Should I proceed?",
context="About to delete 50 files",
node_source="cleanup_node"
)Best Practices
- Use appropriate risk levels - Set
security_levelorrisk_levelaccurately - Provide clear context - Include enough information for informed decisions
- Handle rejections gracefully - Always have a fallback path
- Use durable execution - Enable
COLLABORATIVEmode for long operations - Timeout handling - Consider what happens if user doesn’t respond
- Use MCP tools - Prefer
ask_humanandrequest_authorizationfor standardized interactions
Next Steps
- Checkpointing - Durable execution details
- Options - Configure HIL behavior
- Streaming - Handle HIL in streams