KDCube
← Our Journal

A second thought should not become a second race. A stop should not leave the conversation broken. These are small expectations at the chat surface and distributed-system contracts underneath it.

The ordered conversation lane in KDCube reached this shape gradually. It matured over months under the native ReAct agent, not as an isolated infrastructure project. Long turns, generated files, external events, followups, retries, and work moving between processors kept testing the same promise from different directions.

The first design did not survive real use unchanged. Followups forced the handoff to widen from one accepted batch to the whole pending lane. Later failures sharpened turn ownership, release, and recovery. Each correction came from something being built on top of the runtime, so the mechanism matured alongside the product it was carrying.

The native KDCube agent, ReAct, gave the mechanism a natural rhythm. Between rounds, it can look at the lane again, bring a correction into the current turn, or stop before starting more work. Under that behavior sit the less visible guarantees: events enter a durable order, one conversation has one executing turn, wake retries preserve one execution, and input that cannot be handled yet remains waiting instead of disappearing.

That foundation was settled before we asked the harder question: can the same conversation contract reach an agent whose loop is not ours?

One conversation, several kinds of loop

A resident agent runs as part of a KDCube app and uses the platform's conversation, identity, streaming, and delivery surfaces. It may still own its iteration through another runtime. A LangGraph graph decides how its nodes advance. A hosted Claude Code process decides when it calls its next tool. Neither loop becomes ReAct because it lives in the same app.

So the integration does not pretend that every runtime can be interrupted in the same place.

The app route and the agent route are separate. The request surface resolves the app, and the accepted processor envelope carries it as routing.bundle_id. Within that app, payload.target.agent_id is normalized into the accepted events' agent_id and selects the agent-scoped ordered lane. Proc invokes the app's single @on_reactive_event door; the app then dispatches by agent_id to the selected agent adapter. Only that adapter decides when later input can reach its model.

The package reaches the agent inside the app; its runtime carries it into the loop The submitted package carries the message, attachments, external events, routing.bundle_id, and payload.target.agent_id. The platform routes bundle id to the app and agent id to that app's ordered conversation lane. Inside the addressed app, the reactive door (@on_reactive_event) starts a scheduled turn when none is running, and agent dispatch hands the package to the one selected agent; when a turn is already in progress, the lane delivers directly to the running turn's listener at the loop's port. From there the agent adapter integrates turn-start input into the agent's own loop, docking at the loop's top port. Native ReAct: the accepted batch enters the decision-rounds loop, and later input enters between rounds; a followup folds into the turn and a steer cancels the phase. LangGraph: a pending-lane snapshot becomes the graph input; later input enters at a stream await, where stop cancels and a followup waits for the next turn. Claude Code: a pending-lane snapshot becomes the CLI prompt; later input enters at PreToolUse, where a followup becomes added context and a stop denies the next tool. Every runtime closes only what its model read; unread input stays pending, in lane order, for the next turn. the package reaches the agent inside the app; its runtime carries it into the loop routing is the same for every agent; only the last step, how input enters the loop, depends on the agent type submitted package message · attachments external_events[] routing.bundle_id payload.target.agent_id platform route bundle_id → the app agent_id → that app’s ordered conversation lane THE ADDRESSED APP reactive door @on_reactive_event starts a turn when none is running agent dispatch agent_id → adapter the package goes to this one agent the selected agent receives the package; its runtime carries it into the loop no turn running: the door starts one · turn in progress: the lane feeds the running turn’s listener at the port below Native ReAct the KDCube loop accepted batch agent adapter decision rounds the agent’s loop between rounds mid-turn, from the lane followup folds into the turn steer cancels the phase closes only what the model read LangGraph the graph owns its loop pending-lane snapshot → graph input agent adapter graph nodes the agent’s loop stream await mid-turn, from the lane stop cancels at an await followup waits for next turn closes only what the model read Claude Code the CLI owns its loop pending-lane snapshot → CLI prompt agent adapter tool calls the agent’s loop PreToolUse mid-turn, from the lane followup → added context stop denies the next tool closes only what the model read unread input is never lost; it stays pending, in lane order, for the next turn
The package reaches the agent inside the app; its runtime carries it into the loop. Each loop admits later input at its own boundary.

Native ReAct: the next round

For ReAct, a followup can fold into the running turn at the next decision boundary, and a steer can cancel the active phase and finalize it.

LangGraph: the cancellable stream

For LangGraph, the safe boundary is the streaming task. A stop cancels that task at an await point; the graph's checkpointer retains completed nodes. An ordinary followup is not injected into the graph. It stays in the lane and joins the next turn.

Claude Code: before the next tool

For Claude Code, the useful boundary is PreToolUse. A followup is supplied as context before the next tool call and the model keeps working. A stop denies that tool call, so it does not run and the model answers with the work it has. The process is not killed.

The shared mechanism observes. The adapter decides what its runtime can safely do.

The difficult part begins after interruption

Reaching a running loop was only the first problem. The second was deciding what the interruption leaves behind.

An event that arrived is not necessarily an event the model read. A Claude Code run may be inside one long tool call and never reach another hook. Only message ids the hook proves it delivered are closed under the current turn. Everything else remains pending. This distinction prevents both failure modes: answering the same correction twice and silently dropping a correction that never reached the model.

Arrival is not delivery An event can arrive while an agent is working without reaching the model. The current turn closes only exact event ids the runtime proves were read. Unread input remains pending. At a stop boundary a bare stop is spent, retained text waits, and later intent wakes at most one turn that folds pending messages in lane order. arrival is not delivery the current turn may account only for input the runtime proves the model read event arrives work is still running Did this runtime prove the model read it? proof is the exact event id delivered at its boundary YES NO current turn owns the exact id close it once; never answer twice the event remains pending nothing silently claimed or dropped stop settles what happened; it does not invent delivery bare stop is spent · retained text waits · later intent wakes one ordered turn
Arrival records intent; delivery evidence decides which turn may account for it.

The conversation reservation has to remain owned for the full run as well. The watcher refreshes only the scheduled reservation belonging to its own turn. Without that owner fence, a long foreign-runtime turn could outlive a freshness window and let a second turn start against the same conversation.

And the runtime's own history must still be valid. Cancelling LangGraph between a model's tool request and the tool result can checkpoint half an exchange. The next provider request then rejects the conversation, one turn after the stop appeared to work. The adapter repairs that state truthfully: the missing result says the run stopped before the tool executed. The model is not asked to infer an outcome that never happened.

Once the lane contract was stable, extending it to both foreign-loop families took one concentrated day. That day was not a shortcut around the hard parts. It was the return on the earlier work: ordering, ownership, exact event ids, reservation release, and next-turn handoff already had one place to live. Each new adapter had to answer only two questions: where can this loop be reached, and how is its state made whole afterward?

What the person experiences

The person can keep speaking while the agent works. KDCube records each message immediately. A runtime that can accept it live receives it at its next safe boundary; another leaves it ordered for the next turn.

Pressing stop closes the current turn deliberately rather than leaving a stream that simply trails off. Work already completed remains visible. A bare stop does not buy an empty follow-up turn. Text the agent did not read is not lost, and several waiting messages become one ordered input when work resumes.

The intended feeling is closer to interrupting a colleague than cancelling a batch job: the current thought reaches a clean boundary, both sides can see where work stopped, and the correction is present when the conversation continues.

There is one honest limit. None of these adapters can preempt the middle of an arbitrary long tool call. The stop is accepted immediately, but it takes effect at the next boundary that runtime exposes: a ReAct decision, a LangGraph await, or a Claude Code tool hook. That boundary is part of the runtime contract, not a delay the event lane can wish away.

Highlights

One conversation stays serialized even when its turn runs on another worker.
Native ReAct and framework-owned resident agents share the lane contract without sharing one loop.
Followup and stop act at the earliest safe boundary each runtime exposes.
Arrived input stays distinct from input the model demonstrably read.
A stop closes the visible turn and leaves framework history valid for the next one.
Pending messages remain ordered and wake at most one next turn.
KDCube Journal · Entry № 25 · 17.08.2026