Skip to main content

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
One line change

waxell.input() has the same signature as Python's input(). Just add waxell. in front.

Parameters​

ParameterTypeDefaultDescription
promptstr""The prompt string (same as input())
actionstr"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​

ParameterTypeDefaultDescription
promptstr""What was shown to the human
channelstr"terminal"Where the interaction happened
actionstr""What kind of interaction
metadatadictNoneArbitrary 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​

ParameterTypeDefaultDescription
promptstr""What was shown to the human
responsestr""What the human replied
channelstr"terminal"Where the interaction happened
actionstr""What kind of interaction
elapsed_secondsfloatNoneTime the human took to respond
metadatadictNoneArbitrary extra context

What Shows in the Trace​

Each human interaction creates an IO span with these attributes:

AttributeValueDescription
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.promptThe prompt textWhat was shown
output_data.responseThe response textWhat the human said
duration_msElapsed timeHow 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​