Human-in-the-Loop (HITL)¶
Hector implements 100% A2A Protocol-compliant human-in-the-loop features, allowing agents to pause execution and wait for user approval or input before proceeding with certain actions.
A2A Protocol Compliance¶
This implementation follows A2A Protocol Section 6.3 for the INPUT_REQUIRED state:
- Tasks transition to
TASK_STATE_INPUT_REQUIREDwhen user input is needed - Multi-turn conversations use the same
taskIdto resume execution - Interaction details are provided in
TaskStatus.update(JSON field name:message) - Standard A2A message structure with
TextPartandDataPart
Use Cases¶
1. Tool Approval¶
Require user approval before executing potentially dangerous operations:
tools:
execute_command:
type: command
enabled: true
requires_approval: true # ⭐ Enable approval
approval_prompt: "Allow command execution: {input}?"
allowed_commands:
- "ls"
- "rm"
- "git"
delete_file:
type: delete_file
enabled: true
requires_approval: true # ⭐ Require approval for deletions
2. Custom Prompts¶
Customize the approval message for each tool:
tools:
write_file:
type: write_file
enabled: true
requires_approval: true
approval_prompt: |
📝 File Write Request
Tool: {tool}
Input: {input}
Do you approve this operation?
Configuration¶
Task Configuration¶
Configure timeout for user input:
agents:
assistant:
llm: "gpt-4o"
task:
backend: "memory" # or "sql"
worker_pool: 5
input_timeout: 600 # Seconds to wait for user input (default: 600 = 10 minutes)
tools:
- "execute_command"
- "write_file"
Tool Configuration¶
Each tool can have approval settings:
tools:
tool_name:
type: "tool_type"
enabled: true
# Human-in-the-loop settings
requires_approval: true # If true, agent pauses for approval
approval_prompt: "Custom prompt" # Optional: Custom approval message
Supported interpolations in approval_prompt:
- {tool} - Tool name
- {input} - Tool input/arguments
How It Works¶
1. Agent Execution Flow¶
User sends message
↓
Agent starts processing (TASK_STATE_WORKING)
↓
Agent wants to call tool requiring approval
↓
Task transitions to TASK_STATE_INPUT_REQUIRED ⭐ A2A standard state
↓
Approval request sent in TaskStatus.message
↓
Agent execution pauses (waits for user response)
↓
User sends response with same taskId ⭐ A2A multi-turn
↓
Task resumes to TASK_STATE_WORKING
↓
Tool executes (if approved) or skips (if denied)
↓
Task completes (TASK_STATE_COMPLETED)
2. A2A Message Structure¶
When a task requires input, the TaskStatus looks like:
{
"id": "task-123",
"status": {
"state": "TASK_STATE_INPUT_REQUIRED",
"message": {
"role": "ROLE_AGENT",
"parts": [
{
"text": "🔐 Tool Approval Required\n\nTool: execute_command\nInput: rm -rf /tmp/old-files\n\nPlease respond with one of: approve, deny, modify"
},
{
"data": {
"interaction_type": "tool_approval",
"tool_name": "execute_command",
"tool_input": "rm -rf /tmp/old-files",
"options": ["approve", "deny", "modify"]
}
}
]
},
"timestamp": "2025-11-07T10:00:00Z"
}
}
Key points:
- ✅ Uses standard TASK_STATE_INPUT_REQUIRED (A2A Protocol Section 6.3)
- ✅ TextPart provides human-readable prompt
- ✅ DataPart provides structured metadata for programmatic parsing
- ✅ No custom protocol extensions needed
Client Usage¶
REST API¶
1. Send Initial Message¶
curl -X POST http://localhost:8080/v1/agents/assistant/message:send \
-H "Content-Type: application/json" \
-d '{
"request": {
"role": "user",
"parts": [{"text": "Delete all temporary files"}]
},
"configuration": {
"blocking": false
}
}'
Response:
{
"task": {
"id": "task-abc123",
"status": {
"state": "TASK_STATE_INPUT_REQUIRED",
"message": {
"role": "ROLE_AGENT",
"parts": [
{"text": "🔐 Tool Approval Required..."},
{"data": {"interaction_type": "tool_approval", ...}}
]
}
}
}
}
2. Respond with Approval¶
A2A Protocol: Use same taskId to resume
curl -X POST http://localhost:8080/v1/agents/assistant/message:send \
-H "Content-Type: application/json" \
-d '{
"request": {
"role": "user",
"taskId": "task-abc123",
"parts": [
{"text": "approve"},
{"data": {"decision": "approve"}}
]
}
}'
Response:
{
"task": {
"id": "task-abc123",
"status": {
"state": "TASK_STATE_COMPLETED",
"message": {
"role": "ROLE_AGENT",
"parts": [{"text": "Files deleted successfully"}]
}
}
}
}
TypeScript Client¶
import { A2AClient } from '@hector/sdk';
const client = new A2AClient('http://localhost:8080');
async function executeWithApproval() {
// Send initial message
let task = await client.sendMessage('assistant', {
role: 'user',
parts: [{ text: 'Delete all temporary files' }]
});
// Check if approval is required (A2A standard state)
while (task.status.state === 'TASK_STATE_INPUT_REQUIRED') {
// Parse approval request from task.status.message
const message = task.status.message;
const textPart = message.parts.find(p => 'text' in p);
const dataPart = message.parts.find(p => 'data' in p);
console.log(textPart.text); // Display to user
// Get user decision
const decision = await prompt('Approve? (approve/deny/modify)');
// Send response with SAME taskId (A2A multi-turn)
task = await client.sendMessage('assistant', {
role: 'user',
taskId: task.id, // ⭐ Same task - resume execution
contextId: task.contextId,
parts: [
{ text: decision },
{ data: { decision: decision } }
]
});
}
console.log('Task completed:', task);
}
Python Client¶
from hector_sdk import A2AClient
client = A2AClient('http://localhost:8080')
def execute_with_approval():
# Send initial message
task = client.send_message('assistant', {
'role': 'user',
'parts': [{'text': 'Delete all temporary files'}]
})
# Handle approval requests (A2A standard INPUT_REQUIRED state)
while task['status']['state'] == 'TASK_STATE_INPUT_REQUIRED':
# Parse approval request
message = task['status']['message']
text_part = next(p for p in message['parts'] if 'text' in p)
data_part = next(p for p in message['parts'] if 'data' in p)
print(text_part['text'])
# Get user decision
decision = input('Approve? (approve/deny/modify): ')
# Send response with same taskId (A2A multi-turn)
task = client.send_message('assistant', {
'role': 'user',
'taskId': task['id'], # ⭐ Resume execution
'contextId': task['contextId'],
'parts': [
{'text': decision},
{'data': {'decision': decision}}
]
})
print(f"Task completed: {task}")
Response Options¶
Users can respond with three options:
1. Approve¶
Execute the tool with original parameters:
{
"role": "user",
"taskId": "task-abc123",
"parts": [
{"text": "approve"},
{"data": {"decision": "approve"}}
]
}
2. Deny¶
Skip the tool execution:
{
"role": "user",
"taskId": "task-abc123",
"parts": [
{"text": "deny"},
{"data": {"decision": "deny"}}
]
}
3. Modify¶
Modify tool parameters (currently experimental):
{
"role": "user",
"taskId": "task-abc123",
"parts": [
{"text": "modify"},
{"data": {
"decision": "modify",
"modified_input": "{\"command\": \"ls -la /tmp\"}"
}}
]
}
Task Cancellation¶
Cancel a running or paused task:
REST API¶
POST /v1/agents/{agent}/tasks/{taskID}:cancel
Effect¶
- Cancels active execution (sends context cancellation signal)
- Clears any waiting input requests
- Transitions task to
TASK_STATE_CANCELLED
Best Practices¶
1. Enable Approval for Dangerous Operations¶
tools:
# Safe tools - no approval needed
read_file:
type: read_file
enabled: true
requires_approval: false # Safe operation
# Dangerous tools - require approval
execute_command:
type: command
enabled: true
requires_approval: true # Potentially dangerous
delete_file:
type: delete_file
enabled: true
requires_approval: true # Irreversible operation
2. Set Reasonable Timeouts¶
agents:
assistant:
task:
input_timeout: 300 # 5 minutes for quick decisions
3. Use Clear Approval Prompts¶
tools:
execute_command:
requires_approval: true
approval_prompt: |
⚠️ Command Execution Request
Command: {input}
This will execute on the server. Approve?
4. Handle Timeout Gracefully¶
If user doesn't respond within input_timeout, the task fails with timeout error.
Advanced: Task Subscription¶
Subscribe to task updates to receive real-time notifications:
GET /v1/agents/{agent}/tasks/{taskID}:subscribe
Response (SSE stream):
event: status_update
data: {"taskId":"task-123","status":{"state":"TASK_STATE_INPUT_REQUIRED",...}}
event: status_update
data: {"taskId":"task-123","status":{"state":"TASK_STATE_WORKING",...}}
event: status_update
data: {"taskId":"task-123","status":{"state":"TASK_STATE_COMPLETED",...}}
Troubleshooting¶
Task Tracking Not Enabled¶
Error: "Tool requires approval but task tracking not enabled"
Solution: Enable task tracking in agent config:
agents:
assistant:
task:
backend: "memory" # or "sql"
Timeout Waiting for Input¶
Error: "timeout waiting for user input"
Solution: Increase input_timeout or respond faster:
agents:
assistant:
task:
input_timeout: 900 # 15 minutes
Task Not Resuming¶
Issue: Sending response doesn't resume task
Check:
1. Are you using the correct taskId?
2. Is task state TASK_STATE_INPUT_REQUIRED?
3. Did the task timeout?
Examples¶
Complete Example Config¶
llms:
gpt-4o:
type: openai
model: gpt-4o
api_key: ${OPENAI_API_KEY}
tools:
execute_command:
type: command
enabled: true
requires_approval: true
approval_prompt: "Execute command: {input}?"
allowed_commands: ["ls", "pwd", "git", "npm"]
write_file:
type: write_file
enabled: true
requires_approval: true
approval_prompt: "Write file with input: {input}?"
read_file:
type: read_file
enabled: true
requires_approval: false # Safe operation
agents:
assistant:
llm: "gpt-4o"
reasoning:
engine: "chain_of_thought"
enable_streaming: true
task:
backend: "memory"
worker_pool: 5
input_timeout: 600
tools:
- "execute_command"
- "write_file"
- "read_file"
Summary¶
✅ 100% A2A Protocol Compliant - Uses standard INPUT_REQUIRED state
✅ No Custom Extensions - Pure A2A message structure
✅ Multi-turn Support - Same taskId resumes execution
✅ Simple Configuration - Just set requires_approval: true
✅ Task Cancellation - Cancel at any time via standard A2A endpoint
The implementation provides secure human-in-the-loop workflows while maintaining full compatibility with the A2A Protocol specification.