Human-in-the-loop¶
Set interrupt: true on an agent to grant it the built-in ask_user tool. When
the agent calls it, the run pauses and the question surfaces to the client; on
resume the same call returns the user's answer.
review_team (delegate):
coordinator ─▶ field_agent (interrupt: true, asks the user)
Config¶
agents:
field_agent:
interrupt: true # grants the built-in ask_user tool
system_prompt: |
Before finalizing anything risky, call ask_user to confirm. If several
details are unclear, ask them together in one ask_user call.
orchestrations:
review_team:
mode: delegate
entry_name: coordinator
connections:
- { agent: field_agent, description: "Gated field work with user confirmation" }
entry: review_team
Full runnable project: examples/18_hitl.
Key behaviours¶
- Interrupts bubble up. A gate fired by a nested sub-agent surfaces to the single top-level handler and pauses the entire run — no matter how deep it is.
- Parallel gated tool calls. An agent can raise several gates in one turn (a multi-question form, or two tool calls). Each becomes its own interrupt with a distinct tool-call id, and the run resumes once all are answered.
- Positions. HITL works for a plain entry agent, a delegate sub-agent, and swarm/graph nodes alike.
Run¶
HITL needs a client with a resume UI — serve it and connect a CopilotKit frontend:
OPENROUTER_API_KEY=... uv run kaboo-serve examples/18_hitl/config.yaml
Tool gates and expiry¶
Beyond ask_user, interrupt.tools lists tool names that pause for approval
before executing. interrupt.ttl_seconds optionally stamps an expiresAt
ISO-8601 timestamp onto every gate interrupt (AG-UI Interrupt.expiresAt), so
clients can render a countdown and servers can expire unanswered approvals —
kaboo itself never auto-expires; enforcement stays with the caller.
agents:
field_agent:
interrupt:
tools: [transition_work_item, delete_work_item]
ask_user: false
ttl_seconds: 86400 # each gate carries expiresAt = now + 24h
Resume protocol¶
On pause, the run finishes with an interrupt outcome listing the pending
interrupts (each with an id). The client resumes by sending, per interrupt,
either {"status": "resolved", "payload": ...} or {"status": "cancelled"}
(see errors & rejection).
For tool gates, a resolved payload of {"status": "approved", "tool_input":
{...}} executes the gated call with the user's edited arguments instead of
the agent's — approving a subset of a bulk operation is one resume, not a
round-trip through the model. A payload of {"status": "cancelled"} rejects
the call just like a cancelled entry.
A gate outlives the process that opened it¶
An approval is the one pause that can last hours, which makes it the pause most
likely to be interrupted by a deploy. The gate itself lives in the agent object's
interrupt state, so a restart between the question and the answer used to strand
it: the user clicked approve and got No agent session found for resume.
Served through create_agui_app, the pending gate now travels on the AG-UI state
channel under kaboo_session and is restored onto whichever agent runs the
resume — including one that has never seen the conversation. A resume therefore
works after a restart, on a second replica, and when the session is rebuilt per
run. Nothing to configure; see
Chapter 7
for the switch that turns it off and why you would.
The exception is a Swarm or Graph entry: strands does not yet persist state for orchestration node agents, so a multi-agent entry still needs the process to stay up between question and answer. A plain agent, with or without delegates, is covered.
Proven by¶
tests/e2e/test_cross_cutting.py::test_ask_user_interrupt_then_resume(plain / delegate / swarm / graph positions).tests/e2e/test_complex.py::test_parallel_interrupts_surface_together_and_resume(two gates in one step, distinct ids, both resumed).tests/e2e/test_cross_cutting.py::test_approval_survives_a_restart_of_the_service(the resuming turn runs on a fresh process that never saw the question).tests/e2e/test_runtime_configs.py::test_approval_survives_the_session_being_rebuilt(the same, within one process, when every run builds its own session).