Consume Events Only at Safe Points
New events wait for the loop boundary and are consumed in batch. Cancellation is manufacturing a safe point early; interruption survives only via trajectory-valid placeholders.
One agent instance can face many events at once: a new user message, a tool result, a timer firing, a peer agent's request for collaboration. Handling them is governed first by discipline, not speed: events are consumed only at the boundary of each loop iteration. Cutting into a step in flight buys you a trajectory nobody can interpret.
The loop boundary is the default safe point
Treat an async agent as a long-running loop: each round takes some events from the input queue, appends them to the trajectory, calls the LLM once, executes the tools it chose, and returns to the top to wait for the next batch — the same structure as a goroutine reading from a channel inside for { select { ... } } ai-agent-book-6. The key property: while the LLM is reasoning or a tool is executing, newly arrived events do not materialize mid-step; they wait in the queue until a safe point — a stretch of reasoning ending, a tool returning. Cancellation follows the same discipline: never sever at an arbitrary moment, but check "was I asked to stop" at safe points, exactly the role ctx.Done() plays in Go. The chapter's whole interaction stack shares one set of primitives: wake-up, safe point, cancellation, preemption, and the fast/slow split.
Three strategies, three ways to treat the safe point
Cancellation-based handling serves urgent events by manufacturing a safe point early, and its flow is fixed in four steps: stop the current operation (cancel the streaming reasoning, signal the running tool), drain the pending queue, append the queued events plus the urgent one to the trajectory end, and re-invoke the LLM on the updated full trajectory ai-agent-book-6. The canonical case: the user types "stop — I said it wrong" while the agent executes a mistaken operation, and the correction is seen before the error completes. Queued handling serves routine events: append to the queue without interrupting, and whenever any tool call returns, check the queue and flush all pending events in one batch — "only results from the last month" arriving mid-search is presented together with the search result, saving a round trip. Parallel handling serves independent lightweight queries: unrelated to the main task, needs a fast answer, cheap to run — only when all three hold, spin up a separate reasoning session, answer immediately, then append the exchange back to the main trajectory explicitly marked "ran in parallel with the main task" so the model does not confuse it.
Urgency first, strategy second
Strategy choice presupposes a classification. Urgent: user.interrupt, supervisor.instruction, agent.interrupt, and urgent external triggers such as system alerts or payment failures. Routine: ordinary user input, agent input, tool.result, timer triggers, regular external triggers ai-agent-book-6. Hard-coded rules hit their ceiling quickly — event semantics decide the handling: "stop immediately" takes cancellation, "what's the weather" takes the parallel path, "send me the report in Chinese" takes the queue. The recommendation is a lightweight classifier LLM acting as the event router, judging the strategy as each event arrives. One caveat about the router itself: it sits on the path of every event, so slower or pricier means global latency.
Interruptions are patched with placeholders
A hard constraint on cancel points: they must sit where a tool or a reasoning step can close safely; an unfinished tool result is represented by an explicit placeholder — success must never be faked ai-agent-book-6. The engineering trick for simulating synchronous-with-async runs is five rules: record the assistant message (thinking, content, tool call) immediately after output; record the tool result only when the tool finishes; if interrupted mid-tool, emit a placeholder response for the unfinished call ("the tool is still running in the background — handle the new event first"), append the interrupting event, and re-invoke; if interrupted mid-thinking, discard the partial thought without writing it; every non-interrupting event stays queued for batch append. The worked example — drafting an email when the user asks about the weather — shows why: the interrupted search_contacts gets its placeholder, the LLM sees a perfectly well-formed trajectory, answers the weather, the real result arrives later, drafting resumes. In the normal state the model sees an ideal synchronous trajectory, which is kindest to models trained on synchronous trajectories; the placeholder is the "necessary compromise" reserved for genuine interruptions. And the compromise has a price: training showed the model that a tool call is followed by a real result almost without exception, so it may later fabricate the missing result and act on it — which is exactly why only truly urgent events interrupt, interrupted tasks must be remembered and resumed, and batches must be considered jointly rather than just the last item. Same enemy as Long Tasks Need Handoffs and End States, measured in seconds instead of hours; and An Answer Is Not Accountability supplies the other half of the placeholder's promise — how far the world actually changed is decided by receipts, not by trajectory memory.
