HumanAgent¶
Prerequisites¶
Before you start, complete the Installation setup. If you’re new to the framework, skim Core Concepts for the mental model.
What Is HumanAgent¶
HumanAgent is a ready-to-use, terminal-based agent that extends BaseAgent to bring humans into agent workflows. It provides a simple CLI for real-time input/output and behaves as a first-class agent in the ecosystem (registration, routing, verification).
At a glance
Start a direct 1:1 terminal chat with
start_interaction()Run as a normal agent with
run()and receive messages routed by the hubUse
send_message()to contact any other agent (human or AI) via the hub
When To Use It¶
Use HumanAgent when you need:
A human to review or approve AI agent outputs before a workflow proceeds
Interactive debugging or demos with live agents in a terminal
A human participant in a multi-agent workflow
Use a custom BaseAgent subclass instead when you need a non-terminal interface (web UI, Slack, email) or fully custom input/response logic. See Custom HumanAgent Implementations for how to extend HumanAgent for those cases.
How To Use It¶
HumanAgent supports three interaction patterns: direct 1:1 chat, passive hub participation, and proactive messaging.
Direct Chat (start_interaction)¶
Start a dedicated, interactive terminal session between a human and an AI agent:
import asyncio, os
from dotenv import load_dotenv
from agentconnect.agents import AIAgent, HumanAgent
from agentconnect.communication import CommunicationHub
from agentconnect.core.registry import AgentRegistry
from agentconnect.core.types import AgentIdentity, ModelProvider, ModelName
async def main():
load_dotenv()
registry = AgentRegistry()
hub = CommunicationHub(registry)
human = HumanAgent(
agent_id="human_01", # Must start with "human_"
name="Alice",
identity=AgentIdentity.create_key_based(),
)
ai = AIAgent(
agent_id="assistant_01",
name="Assistant",
identity=AgentIdentity.create_key_based(),
provider_type=ModelProvider.OPENAI,
model_name=ModelName.GPT4O_MINI,
api_key=os.getenv("OPENAI_API_KEY"),
)
await hub.register_agent(human)
await hub.register_agent(ai)
ai_task = asyncio.create_task(ai.run())
try:
await human.start_interaction(ai) # Blocks until exit/quit/bye
finally:
await ai.stop()
await hub.unregister_agent(human.agent_id)
await hub.unregister_agent(ai.agent_id)
if __name__ == "__main__":
asyncio.run(main())
Standalone vs Connected
start_interaction() is interactive and blocks until exit. Register both agents with the hub first so routing, verification, and capabilities work as expected.
Hub Participant (run)¶
Register the human as a normal agent and start run(). Any agent that knows the human’s agent_id can send messages to them through the hub:
human = HumanAgent(
agent_id="human_reviewer",
name="Reviewer",
identity=AgentIdentity.create_key_based(),
)
ai = AIAgent(
agent_id="analyst_01",
name="Analyst",
identity=AgentIdentity.create_key_based(),
provider_type=ModelProvider.OPENAI,
model_name=ModelName.GPT4O_MINI,
api_key=os.getenv("OPENAI_API_KEY"),
)
await hub.register_agent(human)
await hub.register_agent(ai)
# Start agent's processing loo
human_task = asyncio.create_task(human.run())
ai_task = asyncio.create_task(ai.run())
# Routes via the hub—no direct reference to the human agent needed
await ai.send_message(
receiver_id="human_reviewer",
content="I've completed my analysis. Please review and approve.",
message_type=MessageType.TEXT,
)
# At this point, the human will see the message in their terminal
# and will be prompted to respond. The script will wait at this point.
Routing via hub
Agents do not need direct references to each other—messages are routed by agent_id through the CommunicationHub.
When a message arrives, the terminal displays the sender and content and prompts for a response. The human’s reply is routed back through the hub to the sender.
Proactive Messaging (send_message)¶
Use send_message to contact any registered agent at any point without waiting for an incoming message:
await human.send_message(
receiver_id="analyst_01",
content="Please run a fresh analysis on today's data.",
)
Parameters & Configuration¶
Required¶
agent_id (str): Unique identifier for the agent
name (str): Human-readable name
identity (
AgentIdentity): Cryptographic identity used to sign/verify
Note
The agent_id for HumanAgent must have a prefix of human_ for distinction.
Optional¶
organization (str, optional): Organization or entity the human represents
response_callbacks (List[Callable], optional): Called after each
send_message()with{"receiver_id", "content", "message_type", "timestamp"}
Effects
A profile is created automatically with
AgentType.HUMANand capabilitiestext_interactionandcommand_executionfor discovery/routingIdentity is used to sign outgoing messages; inbound messages are verified before prompting
Terminal Behavior¶
start_interaction mode¶
Human Agent human_01 starting interaction with assistant_01
Exit with 'exit', 'quit', or 'bye'
Loading...
You: Hello, can you summarize the project status?
----------------------------------------
Assistant:
Here is the current project status summary...
----------------------------------------
You: exit
Type any text → sent as a
TEXTmessage to the target agentexit,quit, orbye→ sends aSTOPmessage and ends the session
run mode¶
analyst_01:
I've completed my analysis. Please review and approve.
----------------------------------------
Type your response or use these commands:
- 'exit', 'quit', or 'bye' to end the conversation
- Press Enter without typing to skip responding
You:
Behavior notes:
Typing a response sends a normal
TEXTmessage back to the senderPressing Enter skips responding
exit|quit|byeends the conversation (aSTOPis sent)If the peer is in cooldown or an error occurs, HumanAgent displays the notice and stays interactive
Advanced Usage¶
Response Callbacks¶
Track human responses programmatically:
def audit_log(response_data: dict) -> None:
print(f"[AUDIT] -> {response_data['receiver_id']}: {response_data['content'][:80]}")
human = HumanAgent(
agent_id="human_reviewer",
name="Reviewer",
identity=AgentIdentity.create_key_based(),
response_callbacks=[audit_log],
)
Callback Data¶
The callback receives a dictionary with:
receiver_id: Agent receiving the human’s messagecontent: Text content of the responsemessage_type: Message type (TEXT, STOP, etc.)timestamp: When the response was sent
Managing Callbacks¶
Add or remove callbacks at runtime:
human.add_response_callback(audit_log)
human.remove_response_callback(audit_log)
Custom HumanAgent Implementations¶
Subclass HumanAgent and override process_message() to replace or augment the terminal with any other input mechanism: web UIs, Slack bots, email approvals, mobile push, and more.
Overriding process_message()¶
Two patterns cover most cases.
Pattern 1: Augment terminal behavior
Keep the standard terminal I/O but add logic before or after it:
from agentconnect.agents import HumanAgent
from agentconnect.core.message import Message
from typing import Optional
class LoggingHumanAgent(HumanAgent):
async def process_message(self, message: Message) -> Optional[Message]:
# Standard terminal I/O; includes BaseAgent validation
response = await super().process_message(message)
if response:
self._audit(response) # Custom post-processing
return response
Pattern 2: Replace terminal I/O with a custom interface
Call super(HumanAgent, self) to jump directly to BaseAgent validation (signature verification, cooldown, STOP handling) and provide your own input source instead of the terminal:
from agentconnect.agents import HumanAgent
from agentconnect.core.message import Message
from agentconnect.core.types import MessageType
from typing import Optional
class CustomInterfaceAgent(HumanAgent):
async def process_message(self, message: Message) -> Optional[Message]:
# BaseAgent validation only—no terminal I/O
base_response = await super(HumanAgent, self).process_message(message)
if base_response:
return base_response
response_text = await self._get_response_from_custom_interface(message)
if not response_text:
return None
return Message.create(
sender_id=self.agent_id,
receiver_id=message.sender_id,
content=response_text,
sender_identity=self.identity,
message_type=MessageType.TEXT,
)
async def _get_response_from_custom_interface(
self, message: Message
) -> Optional[str]:
# Post to a webhook, query a web API, read from a queue, etc.
...
Which super() to call
await super().process_message(message)— delegates toHumanAgent(keeps terminal I/O)await super(HumanAgent, self).process_message(message)— skipsHumanAgent, callsBaseAgentdirectly (no terminal I/O, full lifecycle handling preserved)
Example: Web-Based Approval Agent¶
Full WebApprovalAgent implementation (click to expand)
Stores each incoming request in memory, waits for a web UI to submit the decision via submit_approval(), then returns the result to the requesting agent.
import asyncio
from typing import Optional
from agentconnect.agents import HumanAgent
from agentconnect.core.message import Message
from agentconnect.core.types import AgentIdentity, MessageType
class WebApprovalAgent(HumanAgent):
def __init__(
self,
agent_id: str,
name: str,
identity: AgentIdentity,
approval_timeout: int = 300,
**kwargs,
):
super().__init__(agent_id, name, identity, **kwargs)
self.pending_approvals: dict = {}
self.approval_timeout = approval_timeout
async def process_message(self, message: Message) -> Optional[Message]:
base_response = await super(HumanAgent, self).process_message(message)
if base_response:
return base_response
request_id = f"{message.sender_id}_{message.timestamp}"
approval_event = asyncio.Event()
self.pending_approvals[request_id] = {
"message": message,
"event": approval_event,
"response": None,
}
# In production: write to a database, fire a webhook, send a notification.
try:
await asyncio.wait_for(
approval_event.wait(), timeout=self.approval_timeout
)
response_content = self.pending_approvals[request_id]["response"]
del self.pending_approvals[request_id]
return Message.create(
sender_id=self.agent_id,
receiver_id=message.sender_id,
content=response_content,
sender_identity=self.identity,
message_type=MessageType.TEXT,
)
except asyncio.TimeoutError:
del self.pending_approvals[request_id]
return Message.create(
sender_id=self.agent_id,
receiver_id=message.sender_id,
content="Approval request timed out",
sender_identity=self.identity,
message_type=MessageType.ERROR,
metadata={"reason": "timeout"},
)
def submit_approval(self, request_id: str, response: str) -> bool:
"""Called by your web framework when the human submits a decision."""
if request_id not in self.pending_approvals:
return False
self.pending_approvals[request_id]["response"] = response
self.pending_approvals[request_id]["event"].set()
return True
Usage:
web_agent = WebApprovalAgent(
agent_id="human_web_approver",
name="Web Approver",
identity=AgentIdentity.create_key_based(),
approval_timeout=600,
)
await hub.register_agent(web_agent)
web_task = asyncio.create_task(web_agent.run())
# When your web API receives a decision:
web_agent.submit_approval(request_id, "approved")
See Messages for the full Message API and available message types.
Other Human-in-the-Loop Patterns¶
Any interface that asynchronously delivers a human response maps onto Pattern 2:
Slack / Discord: Post to a channel; wait for an emoji reaction or thread reply
Email approval: Send with unique approve/reject links; wait for webhook callback
Mobile push: Send via Firebase/APNs; await response from mobile backend
Database queue: Write the request; poll or listen for status
Voice call: Initiate via Twilio; capture DTMF tones (1 = approve, 2 = reject)
Implementation Notes¶
When building a custom HumanAgent:
Call
await super(HumanAgent, self).process_message(message)first. Return its result immediately if non-NoneHandle timeouts—return
MessageType.ERRORwithmetadata={"reason": "timeout"}Clean up pending entries to prevent memory leaks
Roadmap & Compatibility Notes¶
The following HumanAgent-specific changes are planned for upcoming releases. Current usage patterns remain valid during the transition.
Identity (future):
DID will become the canonical agent identifier (agent_id == identity.did). The "human_" prefix convention will evolve—agent type will be carried in the profile rather than the ID string. A migration guide will accompany this change.
Lifecycle (future):
BaseAgent.run() and its internal message queue will be replaced by a hub-managed mailbox with worker pulls. The terminal interaction experience stays equivalent. Hub registration will shift from hub.register_agent(agent) to agent-initiated agent.register(hub).
CLI-first human interaction (planned):
agentconnect human chat --to <agent_id> and agentconnect human send --to <agent_id> "message" are planned CLI commands backed by a HubClient. Humans will be able to participate in a running ecosystem without embedding Python code or sharing the agent process.
Message model (future):
Message v2 adds first-class correlation_id and typed payloads. The process_message override patterns shown here remain valid. The Message class will gain convenience reply methods (message.reply(), message.reply_text()) that simplify constructing responses.
Next Steps¶
Now that you understand HumanAgent, explore:
BaseAgent - The abstract foundation and custom agent patterns
AIAgent - AI-powered agents with built-in collaboration tools
Setting Up an Agent Network - Building multi-agent systems with human oversight
Agent Toolbox: Discover, Delegate, Track - Designing workflows with approval checkpoints
Messages - Message types, construction, and semantics
Examples - Complete runnable examples