Human-in-the-Loop
Interactive agents regularly pause for human input — terminal prompts, CLI confirmations, Slack messages, coding agent "proceed?" checks. Without instrumentation, these interactions are invisible in the trace. The agent asks something, the human responds, time passes — none of it shows up.
Waxell provides three ways to capture human interactions, from zero-effort to fully custom.
waxell.input() — Drop-in Replacement
The simplest option. Swap Python's built-in input() for waxell.input() and every terminal prompt is auto-captured with timing:
import waxell_observe as waxell
waxell.init()
@waxell.observe(agent_name="my-agent")
async def chat_agent():
while True:
# Before: user_input = input("> ")
user_input = waxell.input("> ") # auto-captured
if user_input.lower() == "quit":
break
response = await call_llm(user_input)
print(response)
That's it. Each call records:
- What was shown — the prompt string
- What the human typed — their response
- How long they took — elapsed time from prompt to response
waxell.input() has the same signature as Python's input(). Just add waxell. in front.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
prompt | str | "" | The prompt string (same as input()) |
action | str | "input" | What kind of interaction (keyword-only) |
waxell.human_turn() — Context Manager
For non-terminal channels — Slack, webhooks, UI dialogs — where the interaction pattern varies:
import waxell_observe as waxell
# Slack approval
with waxell.human_turn(prompt="Deploy to prod?", channel="slack", action="approval") as turn:
response = await wait_for_slack_reaction(channel, timeout=300)
turn.set_response(response) # "approved" / "denied"
# GitHub PR review
with waxell.human_turn(prompt="Review PR #42", channel="github", action="review") as turn:
review = await poll_for_review(pr_number=42)
turn.set_response(review.state) # "approved" / "changes_requested"
# Custom UI dialog
with waxell.human_turn(prompt="Select target environment", channel="ui") as turn:
selection = await show_dialog(options=["staging", "production"])
turn.set_response(selection)
The context manager auto-captures elapsed time. Call turn.set_response() before exiting to record what the human said.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
prompt | str | "" | What was shown to the human |
channel | str | "terminal" | Where the interaction happened |
action | str | "" | What kind of interaction |
metadata | dict | None | Arbitrary extra context |
waxell.human_interaction() — One-Shot
When you already have all the data (e.g. from a webhook callback or log):
import waxell_observe as waxell
waxell.human_interaction(
prompt="Approve budget increase?",
response="approved",
channel="email",
action="approval",
elapsed_seconds=142.5,
metadata={"approver": "finance@company.com"},
)
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
prompt | str | "" | What was shown to the human |
response | str | "" | What the human replied |
channel | str | "terminal" | Where the interaction happened |
action | str | "" | What kind of interaction |
elapsed_seconds | float | None | Time the human took to respond |
metadata | dict | None | Arbitrary extra context |
What Shows in the Trace
Each human interaction creates an IO span with these attributes:
| Attribute | Value | Description |
|---|---|---|
kind | "io" | Same kind as user_message / agent_response |
io.direction | "interactive" | Distinguishes from "inbound" / "outbound" |
io.channel | "terminal", "slack", etc. | Where the interaction happened |
io.action | "input", "approval", etc. | What kind of interaction |
input_data.prompt | The prompt text | What was shown |
output_data.response | The response text | What the human said |
duration_ms | Elapsed time | How long the human took |
Safe Outside a Context
All three functions are no-ops when called outside a WaxellContext — no errors, no side effects:
# No active context — falls back to plain input()
answer = waxell.input("Name: ") # works, just not traced
# No active context — silently ignored
waxell.human_interaction(prompt="test", response="yes")
Auto-Capture in Approval Workflows
When using approval workflows, human interactions are captured automatically. The on_policy_block handler is wrapped in a human_turn — you don't need to add any instrumentation code:
@waxell.observe(
agent_name="my-agent",
workflow_name="delete",
enforce_policy=True,
on_policy_block=waxell.prompt_approval, # auto-captured as human_turn:approval
)
async def delete_records(table: str):
...
See Approval Workflows for the full guide.
Next Steps
- Approval Workflows — Handle policy blocks with human approval
- Conversation Tracking — Auto-captured user messages and agent responses
- Policy & Governance — Pre-execution and mid-execution policy checks