Skip to content

A Stop keeps the reply it cut off; a stopped step's unrecorded state writes are dropped (#181) ​

A Stop (interrupt) now keeps the reply the stopped step was writing, as a flagged assistant row. A step that is stopped no longer commits any state write that no recorded row backs. This is a major release of @helix-agents/runtime-js, @helix-agents/runtime-cloudflare, @helix-agents/runtime-temporal and @helix-agents/runtime-dbos, and a minor release of @helix-agents/core and @helix-agents/ai-sdk.

What changes on an interrupt ​

  • The reply is kept. The interrupted run's history ends with an assistant row carrying metadata.helixInterrupted = { stop: 'interrupt' }. It holds the answer text and the tool calls, but no reasoning or parts. Each of its calls is paired with a run_stopped not-executed result. An empty reply keeps no row.
    • On runtime-js, the Cloudflare Durable Object and Cloudflare Workflows, the row holds the text the client saw, whatever the model call answered when the stop cut it off. That can be the cut-stream text, a cancelled error, a provider error raised while the call was aborted, or a throw. When no text streamed, no row is kept and nothing fails.
    • On Temporal and DBOS, the model call is not cut short, so the row holds the complete reply.
  • A stop writes no error chunk. On runtime-js and the CF Durable Object, a model call that answers or throws an error after the stop aborted it no longer streams an error chunk, as on Cloudflare Workflows. A stock useChat stops reading at one, so the client would show a failure where the run was interrupted.
  • An abort keeps nothing, as before. On runtime-js and the CF Durable Object, a model call that answers an error after its run was aborted now ends its step as an abort. The run still ends failed with the stop's framework_cancelled detail, and onAgentFail still fires. Only an outputSchema agent's forced call changes: after a signal-only abort it no longer commits its forced-completion transition (the phase, its forced_completion chunks, realtime memory extraction).
  • The stopped step is still not committed as a step. It keeps its step_discarded stream marker, and the kept row lands in a separate append commit.

The rule for state writes ​

A state write persists if and only if the row that records its work persists:

  • Dropped: the writes of a step that is stopped, for both stop kinds. This covers its afterLLMCall writes, and those of every tool whose result is not recorded (the call is paired run_stopped, or its step commit was refused).
  • Kept: the writes of a call whose recorded result survives the stop, as before (a finishWith or approved tool that ran, a child outcome that arrived).

Before this release, a soft interrupt on runtime-js and the Cloudflare Durable Object kept the stopped step's in-memory writes. Cloudflare Workflows kept an afterLLMCall write when the stop was met at the step commit.

What to check ​

  • Hooks and tools that write state during a step that can be stopped. Their writes are now dropped with the step. Undoing side effects outside helix state (an HTTP call, your own database) is the hook's or tool's own concern.
  • Code that reads an interrupted session's history. Expect an assistant row at the end of an interrupted turn. Detect it with isInterruptedMessage(message) from @helix-agents/core, never by reading the field yourself.
  • A message-less continue. resume({ mode: 'continue' }) omits every interrupted reply that no user message follows (with the results paired to its calls) from the model input, in each of its steps, so the model answers afresh instead of continuing mid-sentence. Once a new user message follows, the reply is sent like any other assistant turn, with no marker.
  • UI on reload. @helix-agents/ai-sdk puts the flag on the reloaded message's metadata.helixInterrupted. That is stock UIMessage metadata, and no new stream chunk is added. Render it as you like, for example with a "stopped" badge.
  • Cloudflare Workflows afterLLMCall writes are staged with the step and land when the step commits. A tool in the same step still reads them. This also fixes a state version mismatch that failed every tool step after an afterLLMCall hook wrote state.

See the hooks guide, State written during a step that is stopped.

Released under the MIT License.