|
| 1 | +# Human-in-the-Loop |
| 2 | + |
| 3 | +Human-in-the-Loop lets you insert human review checkpoints during agent execution. When the agent is about to call tools, you can pause for user confirmation before proceeding. |
| 4 | + |
| 5 | +## Two Pause Points |
| 6 | + |
| 7 | +Agent execution has two phases: "reasoning" and "acting". You can pause at either point: |
| 8 | + |
| 9 | +**Pause after reasoning**: After the model decides which tools to call, but before execution. You can see the tool names and parameters, letting users decide whether to allow execution. |
| 10 | + |
| 11 | +**Pause after acting**: After tool execution completes, but before the next reasoning iteration. You can see the results, letting users decide whether to continue. |
| 12 | + |
| 13 | +## Example: Confirming Sensitive Operations |
| 14 | + |
| 15 | +This example shows how to require user confirmation before executing sensitive operations like deleting files or sending emails: |
| 16 | + |
| 17 | +```java |
| 18 | +// 1. Create confirmation hook |
| 19 | +Hook confirmationHook = new Hook() { |
| 20 | + private static final List<String> SENSITIVE_TOOLS = List.of("delete_file", "send_email"); |
| 21 | + |
| 22 | + @Override |
| 23 | + public <T extends HookEvent> Mono<T> onEvent(T event) { |
| 24 | + if (event instanceof PostReasoningEvent e) { |
| 25 | + Msg reasoningMsg = e.getReasoningMessage(); |
| 26 | + List<ToolUseBlock> toolCalls = reasoningMsg.getContentBlocks(ToolUseBlock.class); |
| 27 | + |
| 28 | + // Pause if sensitive tools are involved |
| 29 | + boolean hasSensitive = toolCalls.stream() |
| 30 | + .anyMatch(t -> SENSITIVE_TOOLS.contains(t.getName())); |
| 31 | + |
| 32 | + if (hasSensitive) { |
| 33 | + e.stopAgent(); |
| 34 | + } |
| 35 | + } |
| 36 | + return Mono.just(event); |
| 37 | + } |
| 38 | +}; |
| 39 | + |
| 40 | +// 2. Create agent |
| 41 | +ReActAgent agent = ReActAgent.builder() |
| 42 | + .name("Assistant") |
| 43 | + .model(model) |
| 44 | + .toolkit(toolkit) |
| 45 | + .hook(confirmationHook) |
| 46 | + .build(); |
| 47 | +``` |
| 48 | + |
| 49 | +## Handling Pause and Resume |
| 50 | + |
| 51 | +When the agent pauses, the returned message contains the pending tool information. Display it to the user and decide next steps based on their choice: |
| 52 | + |
| 53 | +```java |
| 54 | +Msg response = agent.call(userMsg).block(); |
| 55 | + |
| 56 | +// Check for pending tool calls |
| 57 | +while (response.hasContentBlocks(ToolUseBlock.class)) { |
| 58 | + // Display pending tools |
| 59 | + List<ToolUseBlock> pending = response.getContentBlocks(ToolUseBlock.class); |
| 60 | + for (ToolUseBlock tool : pending) { |
| 61 | + System.out.println("Tool: " + tool.getName()); |
| 62 | + System.out.println("Input: " + tool.getInput()); |
| 63 | + } |
| 64 | + |
| 65 | + if (userConfirms()) { |
| 66 | + // User confirmed, continue execution |
| 67 | + response = agent.call().block(); |
| 68 | + } else { |
| 69 | + // User declined, return cancellation |
| 70 | + Msg cancelResult = Msg.builder() |
| 71 | + .role(MsgRole.TOOL) |
| 72 | + .content(pending.stream() |
| 73 | + .map(t -> ToolResultBlock.of(t.getId(), t.getName(), |
| 74 | + TextBlock.builder().text("Operation cancelled").build())) |
| 75 | + .toArray(ToolResultBlock[]::new)) |
| 76 | + .build(); |
| 77 | + response = agent.call(cancelResult).block(); |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +// Final response |
| 82 | +System.out.println(response.getTextContent()); |
| 83 | +``` |
| 84 | + |
| 85 | +## Quick Reference |
| 86 | + |
| 87 | +**Pause methods**: |
| 88 | +- `PostReasoningEvent.stopAgent()` — Pause after reasoning |
| 89 | +- `PostActingEvent.stopAgent()` — Pause after acting |
| 90 | + |
| 91 | +**Resume methods**: |
| 92 | +- `agent.call()` — Continue executing pending tools |
| 93 | +- `agent.call(toolResultMsg)` — Provide custom tool result and continue |
| 94 | + |
| 95 | +**Check pause reason**: |
| 96 | +- `response.getGenerateReason()` returns `REASONING_STOP_REQUESTED` or `ACTING_STOP_REQUESTED` |
0 commit comments