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
| Before | After |
|---|---|
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 fresh | Continues on the preserved session (conversation memory retained) and returns the fresh typed output inline |
failed / terminated child re-consult | Unchanged — 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__sendMessageto acompletedchild returns anis not activeerror, 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
maxStepslow 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 boundedforloop / max-rounds guard) — seemaxStepsis a PER-TURN budget.
Summary
| # | Change | Type | Action required |
|---|---|---|---|
| 1 | completed companion re-consult continues | Behavior change | Terminate-first if you relied on spawn-to-reset; update tests |
| 2 | +1 synthetic __finish__ tool result | Behavior change | Loosen exact message-count/array assertions |
| 3 | Empty companion message rejected | Behavior change | Update any test that sent an empty message |
| 4 | Companion name ≤ 128 chars | Failure-shape | None (no previously-passing name fails) |
| 5 | maxSteps per-turn for continuations | Bug fix | None (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.