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