Human-in-the-Loop Agent¶
HITLAgent adds approval gates to the ReAct loop. Before every tool call, a review_fn is called with a HITLStep describing the proposed action. The reviewer returns a Verdict — approve, reject, or approve-with-override.
This is the recommended pattern for any agentic system that touches real-world state (databases, APIs, files, external services).
Usage¶
from brain.patterns import HITLAgent, HITLStep, Verdict
def my_reviewer(step: HITLStep) -> Verdict:
print(f"\nAgent wants to: {step.action}({step.action_input})")
choice = input("Allow? [y/n]: ").strip().lower()
return Verdict(approved=choice == "y")
agent = HITLAgent(
tools={"delete_record": db.delete, "send_email": mailer.send},
review_fn=my_reviewer,
)
result = agent.run("Archive all records older than 90 days")
print(result.answer)
print(f"Approved {result.approved_steps}, rejected {result.rejected_steps}")
Automated policy reviewers¶
from brain.patterns import block_tools, block_all, allow_all
# Block specific destructive tools
agent = HITLAgent(tools={...}, review_fn=block_tools("delete_record", "truncate_table"))
# Stop immediately on first rejection
agent = HITLAgent(tools={...}, review_fn=my_policy, stop_on_rejection=True)
# Override the action input (e.g. sanitise a query)
def sanitise(step: HITLStep) -> Verdict:
safe_input = step.action_input.replace("DROP", "")
return Verdict(approved=True, override_input=safe_input)
API Reference¶
brain.patterns.human_in_loop.HITLAgent
¶
HITLAgent(tools: dict[str, Tool] | None = None, review_fn: ReviewFn | None = None, provider: LLMProvider | None = None, max_steps: int = 8, stop_on_rejection: bool = False, system_prompt: str | None = None)
Bases: BasePattern
ReAct agent with human-in-the-loop approval gates.
Before every tool call the agent proposes, review_fn is called with a
HITLStep describing the proposed action. If the reviewer returns
Verdict(approved=False), the step is skipped, the agent is told the
action was rejected, and the loop continues.
If stop_on_rejection=True (default: False), the first rejection
terminates the run immediately.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tools
|
dict[str, Tool] | None
|
Mapping of tool name → callable(str) -> str. |
None
|
review_fn
|
ReviewFn | None
|
Callable that receives a |
None
|
provider
|
LLMProvider | None
|
LLMProvider instance; if None, uses LocalEchoProvider. |
None
|
max_steps
|
int
|
Maximum iterations before giving up. |
8
|
stop_on_rejection
|
bool
|
If True, the first rejection ends the run. |
False
|
system_prompt
|
str | None
|
Override the default ReAct system prompt. |
None
|
Source code in brain/patterns/human_in_loop.py
run
¶
run(task: str, **kwargs: Any) -> HITLResult
Run the HITL agent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
task
|
str
|
The question or instruction for the agent. |
required |
Returns:
| Type | Description |
|---|---|
HITLResult
|
HITLResult (extends PatternResult) with approval/rejection counts. |
Source code in brain/patterns/human_in_loop.py
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 | |
brain.patterns.human_in_loop.HITLResult
dataclass
¶
HITLResult(answer: str, steps: list[Step] = list(), iterations: int = 0, ok: bool = True, error: str | None = None, metadata: dict[str, Any] = dict(), approved_steps: int = 0, rejected_steps: int = 0, rejection_reasons: list[tuple[int, str]] = list(), stopped_by_reviewer: bool = False)
Bases: PatternResult
PatternResult extended with HITL-specific metrics.
Attributes:
| Name | Type | Description |
|---|---|---|
approved_steps |
int
|
Number of steps the reviewer approved. |
rejected_steps |
int
|
Number of steps the reviewer rejected. |
rejection_reasons |
list[tuple[int, str]]
|
List of (step_index, reason) tuples for rejections. |
stopped_by_reviewer |
bool
|
True if the run stopped because of a rejection. |
brain.patterns.human_in_loop.HITLStep
dataclass
¶
HITLStep(step_index: int, thought: str, action: str, action_input: str, conversation_so_far: list[dict[str, Any]] = list())
A proposed step surfaced to the human reviewer.
Attributes:
| Name | Type | Description |
|---|---|---|
step_index |
int
|
Which iteration this is (0-based). |
thought |
str
|
The agent's reasoning text. |
action |
str
|
The tool name the agent wants to call. |
action_input |
str
|
The argument the agent wants to pass. |
conversation_so_far |
list[dict[str, Any]]
|
The full message history up to this point. |
brain.patterns.human_in_loop.Verdict
dataclass
¶
A reviewer's decision on a proposed agent step.
Attributes:
| Name | Type | Description |
|---|---|---|
approved |
bool
|
Whether the step is allowed to proceed. |
override_input |
str | None
|
If set, replace the agent's |
reason |
str
|
Optional human-readable explanation for the decision. |
brain.patterns.human_in_loop.allow_all
¶
brain.patterns.human_in_loop.block_all
¶
Reviewer that rejects every step. Useful for testing rejection paths.
brain.patterns.human_in_loop.block_tools
¶
Return a reviewer that rejects specific tool names and approves the rest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*tool_names
|
str
|
Tool names to block. |
()
|
Returns:
| Type | Description |
|---|---|
ReviewFn
|
A ReviewFn callable. |
Source code in brain/patterns/human_in_loop.py
When to Use¶
| Situation | Recommendation |
|---|---|
| Agent touches real-world state | HITLAgent |
| Compliance / audit requirements | HITLAgent |
| Automated safety policy | HITLAgent + block_tools |
| No human in the loop needed | ReActAgent or StreamingReActAgent |
| HITL + streaming UI | Combine HITLAgent with a custom review_fn that yields events |