Skip to content

Migrating to Persistent Companion Continuation

Overview

Persistent companions (sub-agents declared via persistentAgents) are now continuable after they complete. Re-consulting a child that has already reached completed no longer throws or deletes the child — it continues the conversation on the child's preserved session (memory retained) and returns a fresh typed output. This is the foundation of the maker → critic loop: a parent produces an artifact, consults a critic, fixes the artifact, then re-consults the same critic — which still remembers the prior round.

This change lands across all five runtimes (JS, Cloudflare Durable Objects, Cloudflare Workflows, Temporal, DBOS) in the same release. Several smaller observability/validation changes ship alongside it. None require a data migration, but a few change observable behavior and may break tests or consumers that depended on the old behavior.

For the full conceptual treatment of the critic loop, see Sub-Agents → Re-consulting a persistent companion and Finishing Agents.


1. Re-consulting a completed companion now CONTINUES (was throw / delete)

What changed

BeforeAfter
companion__sendMessage to a completed child threw "... is not active (status: completed)"Continues on the preserved session, returns { delivered: true }; the child retains its prior turns
companion__spawnAgent re-using a completed child's name deleted the child session and started freshContinues on the preserved session (conversation memory retained) and returns the fresh typed output inline
failed / terminated child re-consultUnchanged — still throws (sendMessage) / delete-respawns (spawnAgent)

Only the completed terminal status changed. A failed or terminated child still errors on sendMessage and still delete-respawns on spawnAgent.

Why it changed

A persistent companion's defining feature is that it retains memory across rounds. Previously a child that finished one round was a dead end — you could read its last output but not continue the conversation. The destructive "completed → throw/delete" behavior made the critic loop impossible: every re-consult either errored or wiped the child's memory of the prior round. The new behavior treats a re-consult of a completed child as the next turn on the same session, so the child remembers what it said before.

MIGRATION NOTE — spawn-same-name is no longer a "reset" mechanism

Breaking behavior change

If you relied on companion__spawnAgent with a previously-used name to destructively reset a completed child (wipe its memory and start fresh), that no longer happens — a completed child is now continued, not reset.

To force a fresh child, terminate it first: call companion__terminateChild({ name }), then companion__spawnAgent with the same name. A terminated (or failed) child still delete-respawns, giving you a clean session. Only completed lost its reset behavior.

What you may need to do

  • Agent prompts that re-spawn-to-reset — if your parent's system prompt tells the LLM to "spawn the same name again to start over," update it to terminate-then-spawn (or just rely on continuation, which is usually what you actually want).
  • Tests asserting the old throw — tests that asserted companion__sendMessage to a completed child returns an is not active error, or that re-spawn deleted the prior session, must be updated to the new continue semantics.

2. Structured-output agents gain one synthetic __finish__ tool result

What changed

Every structured-output (outputSchema) agent's persisted history now contains one additional message: a role: 'tool' result for the auto-injected __finish__ tool call, with content {"acknowledged":true}. This makes the persisted transcript a valid LLM history (a tool_use without a matching tool_result is rejected by real providers), which is what makes a completed structured-output session resumable in the first place.

This holds on every runtime now (JS, Temporal, DBOS, Cloudflare DO + Workflows). Previously the heal was JS-only, so the extra message appeared only on the JS runtime.

Why it changed

A completed structured-output agent ended its transcript with a dangling __finish__ tool_use and no tool_result. Continuing such a session (a follow-up turn, or — now — a companion re-consult) sent a malformed transcript that providers like Anthropic reject with a 400. The synthetic {"acknowledged":true} result closes the tool_use, so the history is always a valid, continuable transcript.

What you may need to do

  • Exact message-count / message-array assertions — any test or consumer that asserts a structured-output agent's persisted history has exactly N messages, or compares the full message array, will now see one extra (well-formed) message. Update the expected count, or filter on message content/role instead of asserting an exact length. On runtimes other than JS this is a new extra message (the heal previously didn't reach them).

3. Empty companion messages are now rejected

What changed

companion__spawnAgent's initialMessage and companion__sendMessage's message previously accepted an empty string. An empty/whitespace consult then reached the LLM as an empty user turn, which some providers (e.g. Anthropic) reject with a 400 — surfacing as a confusing provider error deep inside the child's run. Both are now bounded .min(1) at the dispatch boundary; an empty value surfaces as a clean { error } tool result (Invalid arguments) instead. This aligns with the framework's existing top-level empty-message rejection in execute().

What you may need to do

  • No change for normal use. If you have a test that intentionally sent an empty companion message and expected it to be forwarded, it now gets a clean tool error instead — update that assertion.

4. Companion child names are bounded to 128 characters

What changed

The resolved child name (from companion__spawnAgent / companion__sendMessage) is now bounded to 128 characters. A longer name previously flowed unchecked into the child session id (${parentSessionId}-agent-${name}) and from there into Temporal / Cloudflare Workflows / DBOS workflow/instance ids — which have platform length limits — surfacing as a confusing deep runtime id-length failure at child start. The dispatcher now surfaces this as a clean { error } tool result instead.

What you may need to do

  • Nothing. No name that previously succeeded now fails — a >128-char name was already a guaranteed hard child-start failure on the durable runtimes (and continuation appends a suffix, pushing it further over the limit). The only change is the failure shape: a clean tool error instead of a deep runtime crash. The bound is length-only; the charset is unrestricted, so existing names and auto-names (e.g. worker-1) are unaffected.

5. maxSteps is per-turn for re-consulted companions and continued sessions

What changed

A re-consult of a persistent companion (and a continued session generally) now gets a fresh per-turn maxSteps budget. maxSteps bounds a single turn / round, not the child's cumulative lifetime across rounds.

Previously the JS (and Temporal) in-process companion continuation reused the loaded child's accumulated stepCount, so a critic re-consulted enough times eventually had zero remaining budget and stopped after a single step — never reaching its fresh verdict. The continuation now resets the per-turn step count, matching root execute() continuation and the other runtimes. customState, workspace refs, and sub-session refs are preserved (memory and custom state survive a continuation) — only the per-turn step budget resets.

What you may need to do

  • Nothing in most cases — this is a fix that makes long critic loops work.
  • If you tuned maxSteps low to limit total work across rounds, note that the cap now applies per round, not per lifetime. Bound your re-consult round count in your own loop policy (a bounded for loop / max-rounds guard) — see maxSteps is a PER-TURN budget.

Summary

#ChangeTypeAction required
1completed companion re-consult continuesBehavior changeTerminate-first if you relied on spawn-to-reset; update tests
2+1 synthetic __finish__ tool resultBehavior changeLoosen exact message-count/array assertions
3Empty companion message rejectedBehavior changeUpdate any test that sent an empty message
4Companion name ≤ 128 charsFailure-shapeNone (no previously-passing name fails)
5maxSteps per-turn for continuationsBug fixNone (re-consult rounds now work); bound round count in your policy

For the critic-loop recipe and the full re-consult semantics, see Sub-Agents and Finishing Agents.

Released under the MIT License.