Pause for approval and resume
Goal: don’t let an agent run a destructive action unsupervised. When a tool is gated by
the permission policy as ASK, the turn pauses, emits
ToolCallApprovalRequired, and hands you a durable RunState. A human approves or denies,
and Runner.resume(state, approved=...) continues — even if the wait spans a process
restart.
This example builds and round-trips that pause state (the offline-testable core).
uv run examples/pause-resume.py#!/usr/bin/env -S uv run --script# /// script# requires-python = ">=3.13"# dependencies = ["kaos-agents>=0.1.28,<0.2"]# ///"""Pause an agent on a sensitive action and resume after approval.
When an agent wants to run a tool the [permission policy](configure-permissions)marks ASK, the turn pauses and emits `ToolCallApprovalRequired` with a durable`RunState`. That state serializes to JSON, so the run can wait for a human — evenacross a process restart — and then `Runner.resume(state, approved=...)` eitherruns the tool or skips it.
This example builds and round-trips that pause state (the offline-testable coreof the flow). Deterministic.
Run it:
uv run examples/pause-resume.py"""
from __future__ import annotations
from kaos_agents.runtime.interrupts import PendingToolCall, RunState
def main() -> RunState: # An agent paused, wanting to run a destructive tool — captured as a durable # RunState the host can persist and review. pending = PendingToolCall( call_id="tc_01", tool_name="kaos-source-delete", arguments=(("path", "/matter/acme/draft.docx"),), reason="destructive: deletes a file", ) state = RunState( run_id="run_42", session_id="acme-review", event_count=7, original_message="Clean up the old drafts.", pending_tool_call=pending, )
# Persist it (e.g. to a queue/DB) — survives a process restart. blob = state.to_json() print(f"paused run serialized to {len(blob)} bytes of JSON")
# Later — possibly in a different process — restore and review it. restored = RunState.from_json(blob) p = restored.pending_tool_call print(f"awaiting approval: tool={p.tool_name!r} reason={p.reason!r}") print(f" run={restored.run_id} session={restored.session_id}") print("operator decides → Runner.resume(state, approved=True) runs the tool") print(" → Runner.resume(state, approved=False) skips it") return restored
if __name__ == "__main__": restored = main() # The pending tool call survives serialization intact — that's what lets a # paused run wait for a human across a restart. assert restored.pending_tool_call.tool_name == "kaos-source-delete" assert restored.run_id == "run_42" assert dict(restored.pending_tool_call.arguments)["path"].endswith("draft.docx")The full flow
- The agent decides to call a tool the policy marks ASK (e.g. a destructive delete).
- The turn emits
ToolCallApprovalRequiredand stops, producing aRunStatewith thePendingToolCall. - The host persists the
RunState(to_json) to a queue or DB and surfaces the approval to a human. Because it’s serializable, the run can wait indefinitely — across restarts, in a different process. - On a decision, the host restores it (
from_json) and callsRunner.resume(state, approved=True)to run the tool, orapproved=Falseto skip it and continue.
This is the durable counterpart to the in-turn permission decision: ALLOW/DENY happen inline; ASK becomes a persistent, resumable interrupt — essential for unattended agents doing consequential work.