Skip to content

Session-unique tool-call ids, a required chunk agentId, and run_paused's run (tree client calls T0, RM-109) ​

This release changes three things a consumer can observe. It is a major release of @helix-agents/runtime-cloudflare (its DO-SQLite stream manager now refuses a chunk it used to accept) and a minor release of @helix-agents/core, @helix-agents/llm-vercel, @helix-agents/runtime-js, @helix-agents/runtime-temporal, @helix-agents/runtime-dbos, @helix-agents/store-memory, @helix-agents/store-redis and @helix-agents/store-cloudflare. The 0.x stores follow the "minor = observable behaviour change" policy: store-cloudflare and store-redis refuse input they used to accept (below), so read this before upgrading them.

1. A tool-call id is unique within its session ​

A provider that repeats a tool-call id (twice in one response, or call_0 in every step) has the repeat stored as <id>_<n>, the smallest free n. The rewrite happens at the step's stream boundary, so the argument-stream frames, the assistant row, the tool results, hooks and child refs all carry the new id. A provider whose ids are unique is never rewritten. Every runtime does it (core's step iterator for runtime-js and the CF Durable Object, CF Workflows executeAgentStep, Temporal runLLMStep, DBOS callLLMStep).

What to check:

  • Code that matched a tool result to its call by the provider's raw id across steps: match by the stored id (read it from the assistant row or the tool_start frame), never by a raw id you predicted.
  • Tests that script a model re-emitting an id the session already holds, to "replay" a committed call: under this release that is a NEW call (stored <id>_1). A recovery replays a committed call through the runtime's own re-entry, never by calling the model again.
  • A custom runtime with its own LLM call site: wire core createCallIdRewriter(history) per model attempt (wrapCallbacks on the stream callbacks, rewriteResult on the step result).

2. A chunk's agentId is required, and a schema-reading manager refuses an invalid chunk ​

A stream chunk's agentId is the session that wrote it ('' for a stream-level chunk the framework synthesizes). No stream manager fills a missing one any more: it used to default to the writer's run id. agentType and timestamp are still filled from the writer.

A manager whose read path parses stored chunks with the chunk schema now checks each chunk it writes the same way, after its own enrichment and before anything is stored (core assertStorableStreamChunk):

WriterA chunk without agentId (or otherwise not schema-valid)
CF stream Durable Object /write (StreamServer)refused: typed 400 validation_error, nothing written; the binding rethrows it typed
RedisStreamManager writerrefused: a thrown validation_error (HelixError, non-retryable), nothing written
DO-SQLite DOStreamManager writerrefused: a thrown validation_error, nothing written, before a sequence is allocated
InMemoryStreamManager writeraccepted as given (its reader does not parse); the StreamChunk type requires agentId

Before this release such a chunk was stored with the writer's run id (memory, Redis, DO-SQLite) or the binding's agentId (the CF stream DO), and a reader that parsed it with the schema could later fail on it.

What to do:

  • An external writer that POSTs to the CF stream Durable Object's /write must send a schema-valid chunk that includes agentId (the writing session, or '') and step. A chunk without them is refused 400 validation_error (it used to be filled from the body's agentId).
  • Code that writes through a Redis or DO-SQLite stream manager with a chunk built outside the type system (a JSON forward, a cast): set agentId (and every required field). It now throws a typed validation_error instead of being stored with the writer's run id.
  • A custom stream manager: keep a chunk's agentId as written (never fill it). If its reader parses stored chunks with streamChunkSchema(), call assertStorableStreamChunk(streamId, chunk) on the enriched chunk before storing it and set untypedWriteBoundary: true in its contract run; the contract suite checks stream.agent-id-is-kept, stream.enriches-agent-type-and-timestamp, stream.synthesized-chunk-is-stream-level and, there, stream.missing-agent-id-is-refused.

The CF Workflows validation-failure tool_end (a regular tool's invalid input, an unregistrable client call) now names the session in agentId, not the run.

3. A synthesized run_paused names the pausing run (RM-109) ​

pauseStream synthesizes a run_paused chunk. Every manager stamped its runId with the run the stream's FIRST writer was created with, so on a multi-run stream a later run's pause named an earlier run; ai-sdk forwards it as data-run-paused.runId. It now names the pause fence's run, else the stream's owner, and is stream-level (agentId: ''). If you read data-run-paused.runId, it is now the run that paused.

Released under the MIT License.