Human Approval

Confirmation, user input, external execution, and approval flows in three demo agents.

Three demo agents exercise the HITL surface end-to-end, each showing a different combination of patterns over the same primitives.

AgentWhat it shows
HelpdeskAll three pause patterns + PII + injection guardrails + audit hook
ApprovalsThe @approval decorator with audit trail
FeedbackUserFeedbackTools and UserControlFlowTools for structured questions

Helpdesk: the full HITL toolkit

1from kern.agent import Agent
2from kern.guardrails import OpenAIModerationGuardrail, PIIDetectionGuardrail, PromptInjectionGuardrail
3from kern.tools.user_feedback import UserFeedbackTools
4
5helpdesk = Agent(
6 id="helpdesk",
7 model=MODEL,
8 db=agent_db,
9 tools=[restart_service, create_support_ticket, run_diagnostic, UserFeedbackTools()],
10 pre_hooks=[
11 OpenAIModerationGuardrail(),
12 PIIDetectionGuardrail(),
13 PromptInjectionGuardrail(),
14 ],
15 post_hooks=[output_guardrail, audit_log],
16)

Where each pattern shows up:

ToolHITL patternWhy
restart_servicerequires_confirmation=TrueRestarting prod is irreversible. Human approves first.
create_support_ticketrequires_user_input=TrueNeed details from the user before creating.
run_diagnosticexternal_execution=TrueDiagnostic runs in another system. The agent gets the result back.
UserFeedbackTools()Structured question to user mid-run"Which service do you want to restart?"

Pre-hooks catch bad input before the model sees it (PII, injection, moderation), and post-hooks catch bad output and write the audit log. The agent never sees raw PII, and every run lands in the audit log.

Approvals: compliance gates

1from kern.tools import tool
2from kern.approval.decorator import approval
3
4@approval(type="required")
5@tool(requires_confirmation=True)
6def process_refund(customer_id: str, amount: float, reason: str) -> str:
7 return charge_refund(customer_id, amount)
8
9@approval(type="audit")
10@tool
11def export_customer_data(customer_id: str) -> str:
12 return get_customer_data(customer_id)

@approval(type="required") blocks the run until a human approves. Audit log captures both the request and the decision.

@approval(type="audit") runs the tool but logs to the audit trail asynchronously. Used when policy says "this needs to be tracked, not gated."

Feedback: structured questions mid-run

1from kern.tools.user_feedback import UserFeedbackTools, UserControlFlowTools
2
3feedback = Agent(
4 id="feedback",
5 tools=[UserFeedbackTools(), UserControlFlowTools()],
6)

UserFeedbackTools lets the agent pause and ask a structured question:

Agent: I need to know your team size. Please pick one: [1-5, 6-20, 21-100, 100+]

The user picks. The run resumes with the answer in scope. No prompt engineering, no hand-rolled flow control.

UserControlFlowTools adds branching. The agent can offer choices and route based on the user's pick:

Agent: Want to (a) keep the current settings, (b) reset to defaults, or (c) customize?

Pre-hooks for input safety

PII detection and prompt injection guardrails are pre-hooks that run before the model sees the user message:

1from kern.guardrails import PIIDetectionGuardrail, PromptInjectionGuardrail
2
3agent = Agent(
4 pre_hooks=[
5 PIIDetectionGuardrail(
6 mask_pii=True,
7 enable_ssn_check=True,
8 enable_credit_card_check=True,
9 enable_email_check=True,
10 enable_phone_check=True,
11 ),
12 PromptInjectionGuardrail(),
13 ],
14)

PII gets masked in place, injection attempts get blocked outright, and the two are commonly used together.

Post-hooks for output safety

Output guardrails and audit logs run after the model produces output. The Helpdesk agent demonstrates both:

1def output_guardrail(run_output, agent):
2 """Block responses that accidentally leak sensitive patterns."""
3 import re
4 sensitive = [r"sk-[a-zA-Z0-9]{20,}", r"postgres://[^\s]+"]
5 for pattern in sensitive:
6 if re.search(pattern, run_output.content or ""):
7 run_output.content = "I'm unable to provide that information."
8 return
9
10def audit_log(run_output, agent):
11 """Audit trail for compliance."""
12 print(f"[AUDIT] Agent={agent.name} Status={run_output.event}")

For audit logs that shouldn't gate the response, use @hook(run_in_background=True). See Human-in-the-Loop for the full pattern.

See it in action

1# Helpdesk
2@Helpdesk restart the auth service # confirmation pause
3@Helpdesk file a ticket for slow API # user input pause
4@Helpdesk run a network diagnostic # external execution pause
5
6# Approvals
7@Approvals refund customer ACME-123 for $500 # blocked until approved
8@Approvals export customer data for ACME-123 # runs, audit-logged
9
10# Feedback
11@Feedback help me pick a deployment region # structured questions

Sources: agents/helpdesk/, agents/approvals/, agents/feedback/

Next

Multi-Agent Teams →