Minor Changes
-
#1758
6b46b04Thanks @threepointone! - Add progress signalling and durable milestones for agent-tool sub-agents
(#1758, rfc-detached-agent-tools §progress, phases 4a + 4b).A sub-agent running as an agent tool (awaited or detached/background) can now
report mid-run progress:// Inside the child sub-agent (e.g. from a tool's execute): await this.reportProgress({ fraction: 0.6, phase: "deploying", message: "Generating menu page…" });
These signals ride the child's own turn stream as a transient
data-agent-progresspart, so they re-broadcast to the parent's connected
clients and surface onAgentToolRunState.progressviauseAgentToolEvents— a
background-runs tray can render a live bar / phase / status line without drilling
in. Highlights:reportProgress({ fraction?, message?, phase?, data? }, { persist? })on
chat agents (@cloudflare/think,AIChatAgent); a no-op with a dev warning on
the baseAgentand when called outside an active agent-tool run. The framework
resolves the run id from the active turn — no threading required. Bursts are
coalesced (latest-wins; afraction >= 1"done" frame always flushes).data
is live-only unless{ persist: true }.onProgress(run, progress)parent hook, fired best-effort from the tail
for both awaited and detached runs.- Latest-snapshot persistence + recovery inspect. The child stores a
progress_json+last_signal_aton its run row and surfaces it through
inspectAgentToolRun().progress, so a rehydrated parent reconstructs progress
after eviction. - Resetting no-progress budget for detached runs. Once a detached child has
reported at least one signal, the backbone gives up if it then goes silent for
detachedNoProgressBudgetMs(default 1h; per-run override via
detached: { noProgressBudgetMs }), surfaced asinterruptedwith the
no-progressreason. A child that never reports is bounded only by the absolute
detachedMaxBudgetMsceiling — we never give up on a run merely for being slow.
Durable milestones (phase 4b)
Naming a
milestonepromotes a signal from the ephemeral tier to a durable
one — there is still only one emit method:// Inside the child sub-agent: await this.reportProgress({ milestone: "sources-gathered", data: { sources: 2 } });
-
Persisted + replayable. Each milestone is one row on the child
(cf_agent_tool_milestones/cf_ai_chat_agent_tool_milestones) with a
monotonic per-runsequence. It rides the stream as a persisted
data-agent-milestonepart (vs. transient progress), so drill-in replay and a
rehydrated parent both see it. Surfaced viainspectAgentToolRun().milestones
andAgentToolRunState.milestones(deduped bysequence). -
onProgressfires for milestones too — the snapshot carries
progress.milestone, so a consumer can branch on milestone vs. ephemeral. -
detached: { onMilestones }chat convenience (@cloudflare/thinkand
AIChatAgent). When a configured milestone lands, the chat agent surfaces an
idempotent synthetic chat message (keyed/idempotent per(runId, name))
before the run finishes. Delivered from both the warm tail and the cold
backbone reconcile; the deterministic id collapses them to at-most-once. Two
modes (thestring[]shorthand defaults to"narrate"):"narrate"(default) — a synthetic assistant message injected directly
(no inference): a cheap, honest status line that does not trigger a turn."react"— a user-role turn so the model responds to the milestone
(steer, start dependent work). Costs a model turn.
detached: { onMilestones: ["preview-ready"] } // narrate (default) detached: { onMilestones: { names: ["needs-approval"], mode: "react" } }
Override the wording via
formatDetachedMilestone(run, milestone). These
synthetic messages carrymetadata.sourceso clients can render them as an
agent event rather than a human turn (the example does this).
The awaitable join point (
awaitAgentToolMilestone, phase 4c) is intentionally
not included here — it is gated behind a design addendum. -
#1758
6b46b04Thanks @threepointone! - Adddetached: { notify: true }support forrunAgentToolon chat agents
(@cloudflare/thinkandAIChatAgent) (#1752).When a detached sub-agent run finishes, a chat agent can inject a message back
into the chat so the model reacts to the result — without you wiringonFinish
by hand:await this.runAgentTool(ResearchAgent, { input, detached: { notify: { source: "research-background" } } });
The injected turn is idempotent per run + terminal status, so an exactly-once
finish never duplicates, while a soft give-up followed by a real late completion
surfaces as two distinct turns. (Think dedupes via asubmitMessages
idempotency key;AIChatAgent, which has no durable-submission layer, persists
under a deterministic message id and runs the follow-up turn inline within the
already-serialized delivery slot.) Usenotify: truefor the default
metadata.source, passnotify: { source }to match your app's message
taxonomy, and overrideformatDetachedCompletion(run, result)to customize (or
suppress) the injected text. -
#1817
7f367d8Thanks @threepointone! -create-thinknow prompts for a starter template when--templateis omitted (and falls back tobasicwhen stdin is non-interactive).npm create thinkandthink initinitialize a git repository — skipping cleanly when the target is already inside one — and scaffold projects with Oxlint/Oxfmt config plus acheckscript. Removes the unused declarativeagent()framework helper and the identity helpers (defineMessengers,defineScheduledTasks,defineChannels) in favor of class-based agents and typed object returns. -
#1790
190ea81Thanks @threepointone! - Addctx.attachReply(attachment)for actions: an advisory, recording-only reply-attachment side-channel surfaced onChatResponseResult.attachments(inonChatResponse) and a publicreplyAttachments(requestId?)getter. Attachments are JSON-normalized, deep-copied on read, capped per turn, and never alter the model-visible tool output; policy callbacks are no-ops, failed executions discard their attachments, approval-gated approved actions support it, and durable-pause approved actions are a v1 no-op. -
#1790
190ea81Thanks @threepointone! - Add a pending-retry lease for the action ledger via the newactionLedgerPendingRetryLeaseMsconfig (default 5 minutes). Apendingledger row left behind by a crashed executor is now reclaimed and re-run once it is stale, but ONLY for actions that declare an explicitidempotencyKey— the key is the developer's assertion that re-running the keyed side effect is safe. Behavior change: such a stale row previously blocked forever withActionPendingError; it now reclaims (refreshingupdated_atin place, stillpending), emitsaction:ledger:reclaimed, and re-runsexecute. Fresh rows, fallbacktool:${toolCallId}keys, and a disabled lease (actionLedgerPendingRetryLeaseMs = false) keep the conservativeActionPendingErrorbehavior. Same-isolate coalescing still wins first, so an in-flight run is never reclaimed. -
#1790
190ea81Thanks @threepointone! - Add a durable action ledger foraction()descriptors so settled server action outputs can be replayed by stable idempotency key without re-running side effects. -
#1790
190ea81Thanks @threepointone! - Generalize the messenger runtime into a public channel surface. AddconfigureChannels()andChannelDefinition(web, voice, messenger, and custom channels) wrappinggetMessengers(), a no-turndeliverNotice()withinformModel, additiveDeliveryTag(kind + turnEnded) on messenger snapshots, per-channel policy (instructions, tool-narrowing,maxTurns) applied as overridable defaults, turn-scoped channel context threaded throughrunTurn(persisted for recovery), reply-attachment rendering at delivery, andchannel:*/notice:*observability events. -
#1790
190ea81Thanks @threepointone! - Add durable-pause approval descriptors:durable-pauseactions now park in a dedicatedcf_think_action_pending_approvalsstore and resume viaapproveExecution/rejectExecutionwith a connection-independent continuation, so a turn can be approved from a dashboard with no live socket (this also fixes codemodeapproveExecutionfrom a dashboard). A unifiedActionApprovalDescriptoris attached to durable-pause, codemode, and approval-gated parts,pendingApprovals()lists all pending approvals for cold-load reconciliation, and an overridabledescribePausedExecution()hook enriches codemode descriptors. -
#1801
c58b401Thanks @threepointone! - Add@cloudflare/think/react, a Think-tuneduseAgentChatwrapper that keepssetMessageslocal-only by default while reusing the shared chat React implementation. -
#1788
3b2af54Thanks @threepointone! -Thinknow annotates and logs row-size compaction the same way
@cloudflare/ai-chatdoes.When a persisted message exceeds the SQLite row-size limit and
Thinkcompacts
its tool outputs or truncates its text parts to fit, the resulting message now
carriesmetadata.compactedToolOutputs(the compacted tool-call IDs) and/or
metadata.compactedTextParts(the truncated text-part indices), andThink
emits aconsole.warndescribing the compaction. The compaction itself is
unchanged —Thinkalready used the shared shape-preservingtruncateToolOutput
compactor — this only adds the previously ai-chat-only annotations/warnings so a
client can tell that a stored message was compacted. Both packages now share one
enforceRowSizeLimitimplementation. -
#1790
190ea81Thanks @threepointone! - Add publicrunTurn(options)facade (Turns RFC step 2): unified turn admission
withmode: "wait" | "submit" | "stream"delegating to the existing
saveMessages,continueLastTurn,submitMessages, andchatmethods.
ExportsTurnInputMessages,RunTurnWait,RunTurnSubmit,RunTurnStream,
RunTurnOptions, andTurnResult. -
#1799
3c2afc9Thanks @threepointone! - AllowrunTurn({ mode: "stream" })to accept array and function inputs, matching the existingwaitmode input surface while preserving the durablesubmitfunction-input guard.
Patch Changes
-
#1788
3b2af54Thanks @threepointone! - Converge recovery forward-progress crediting betweenAIChatAgentandThink.Both hosts now credit the recovery no-progress counter through one shared, host-agnostic rule (
shouldCreditStreamProgress): a progress milestone (a started text/reasoning segment or a settled tool input/output) credits unconditionally, and mid-segment streaming deltas (text-delta/reasoning-delta/tool-input-delta) credit at most once per throttle window via a per-isolateStreamProgressCreditThrottle. PreviouslyAIChatAgentcredited only on chunk-type milestones whileThinkcredited on its flush cadence, so a long single content segment spanning repeated crashes could read as "no progress" underAIChatAgentand false-fire itsno_progress_timeout. The new rule is never coarser than either host's prior cadence, so it can only delay or avoid a false no-progress timeout, never hasten give-up. -
#1803
c476265Thanks @threepointone! - Fix AI SDKstatusgetting stuck after a reconnect that races a turn's
pre-stream window (#1784).A turn is "accepted but pre-stream" while it is queued, debouncing, or awaiting
async setup before its resumable stream starts. A client that connected or sent
aSTREAM_RESUME_REQUESTin that window was answered withSTREAM_RESUME_NONE
("nothing to resume"), so its short resume probe resolvednulland AI SDK
statussettled onreadyeven though the server went on to stream — leaving
the UI unable to render the in-flight turn until a full remount.This adds a shared
PreStreamTurnstracker (agents/chat) and a new
server→clientcf_agent_stream_pendingframe:- The resume handshake now parks resume requests that arrive during the
pre-stream window and emitsSTREAM_PENDING("keep waiting") instead of
STREAM_RESUME_NONE, then flushes parked connections into the normal
STREAM_RESUMINGhandshake once the stream actually starts (and releases them
withSTREAM_RESUME_NONEif the turn is superseded/cleared before streaming). - On
STREAM_PENDINGthe client transport extends its resume probe from the
5s fast-path to a 60s backstop so the probe stays open across the gap. useAgentChatre-probes the stream on a transparent socket reopen (e.g. a
1006 reconnect that does not remount the component) sostatusrecovers.- Continuation affinity is relaxed via an optional
isConnectionPresenthost
hook so a transparent reconnect (whose connection id changed) can resume a
continuation whose original owner connection is gone.
Wired into both
AIChatAgentand@cloudflare/think.The pre-stream tracker is in-memory only; it is hibernation-safe because a turn
in its pre-stream window is an unresolved message-handler promise that pins the
Durable Object in memory, so eviction only happens once a stream is durably
recorded (and resumes viaResumableStream) or the turn has finished. Skipped
turns (supersede/generation change) settle without releasing parked
connections, so a client parked during the window survives onto the successor
turn instead of being cut loose by a prematureSTREAM_RESUME_NONE. - The resume handshake now parks resume requests that arrive during the
-
#1788
3b2af54Thanks @threepointone! - Recovery give-up now resolves the orphaned stream by newest metadata row.The stable-timeout/error give-up path that terminalizes an exhausted recovery
turn previously resolved the turn's orphaned stream id with an in-memory
first-match scan over all stream metadata, while the wake (restart) path already
used the newest durable row keyed by the recovery-root request id. These two
lookups are now a single seam, so both paths surface the same partial — the
newest stream the turn produced — when a request id spans more than one
recovery attempt. Single-attempt turns (one stream row per request id) are
unaffected. -
#1794
b6ad4d5Thanks @threepointone! - Extract transcript repair into a sharedagents/chatprimitive.@cloudflare/think's_repairToolTranscriptParts— which flips an interrupted
tool call (atool-*/dynamic-toolpart with no settled result, left behind
when a stream was cut off mid-flight) into an errored tool-result so the next
provider call doesn't 400 withAI_MissingToolResultsError, and normalizes
malformed toolinput— now lives once as the shared,@internal
repairInterruptedToolPartsprimitive (plus thetoolPartHasSettledResult
terminal-state check) inagents/chat.The primitive is pure (returns a new messages array plus repair stats; never
touches storage, broadcast, or events) and is parameterized by an overridable
repairParthook plus an optionalshouldRepair(part)skip predicate (defaults
to repairing every interrupted part), so both AI-SDK chat hosts can run repair
logic before re-entering inference on a recovered turn — a host whose default
errors the part (ai-chat) usesshouldRepairto leave a part still awaiting a
client interaction verbatim.@cloudflare/thinkdelegates through its existing
repairInterruptedToolParthook with noshouldRepair(repairs everything) — a
pure internal refactor with no observable behavior or API change; its suites pass
unchanged. -
#1772
d4f27feThanks @mattzcarey! - Include each package's documentation in its published package. -
#1790
190ea81Thanks @threepointone! - Add stable approval descriptors for Think actions and preserve approval descriptor metadata on chat tool parts. -
#1790
190ea81Thanks @threepointone! - Harden action approval and authorization edge cases around approved inputs and continuation rechecks. -
#1790
190ea81Thanks @threepointone! - Add action permission metadata and default-full-grant authorization hooks for Think actions. -
#1790
190ea81Thanks @threepointone! - Add theaction()descriptor andgetActions()hook for compiling guarded
server actions into Think tools. -
#1790
190ea81Thanks @threepointone! - Harden Think action descriptors with schema-inferred inputs and JSON-safe output
normalization. -
#1790
190ea81Thanks @threepointone! - Route Think turn entry points through a shared internal_admitTurnspine and
throw a clear error for nested blocking turn admissions that previously could
deadlock. -
#1797
f599892Thanks @threepointone! - Fix: a recovered agent-tool child turn now re-binds its run row to the
recovery turn's request id, so a healthy long-running child is no longer
abandoned asinterruptedafter a deploy.When a facet running as an agent-tool child was interrupted mid-run (e.g. a
deploy evicted it), its recovery continuation (continueLastTurn/
_retryLastUserTurn) minted a fresh request id but left
cf_agent_tool_child_runs.request_idpointing at the pre-eviction turn. Frame
attribution (_agentToolRunForRequest) then failed, so the recovered turn's
broadcast frames never reached the parent's re-attach tail; the parent saw no
forward progress and sealed a still-advancing childinterruptedonce its
no-progress budget elapsed. The recovery paths now re-bind the child-run row
(and the in-memory attribution map) to the current turn's request id, keeping
frames flowing across recovery so the parent re-attaches and follows the child
to its real terminal. -
#1797
f599892Thanks @threepointone! - Fix: a recovered pre-stream retry turn now re-applies per-channel policy.continueLastTurnalready re-resolved the channel from the persisted user
message (metadata.channel) so a recovered partial turn re-applied its
channel's instructions / tool narrowing. The pre-stream retry path
(_retryLastUserTurn, used by_chatRecoveryRetry) admitted the recovered turn
without re-resolving the channel, so an interrupted-before-streaming turn was
retried with the default policy instead of the channel's — even though the
metadata.channelstamp survived. It now re-resolves and re-applies the channel
on both recovery paths, matching the documented invariant. -
#1790
190ea81Thanks @threepointone! - Addchat:turn:startandchat:turn:finishobservability events for Think
turn execution. -
#1790
190ea81Thanks @threepointone! -Think.waitUntilStable()now waits out an armed-but-unfired auto-continuation
before reporting stable, converging onto@cloudflare/ai-chat.Previously, when a turn ended with no pending human/client interaction,
waitUntilStable()reported stable immediately — even if an auto-continuation
was armed (its ~50ms coalesce timer still pending, or its completeness drain in
flight). In that window idle eviction or chat recovery could act on a transcript
that was about to be continued.Thinknow mirrors@cloudflare/ai-chat: while
a continuation is armed (pending && !pastCoalesceand the shared
AutoContinuationControllerreports armed),waitUntilStable()reports
not-stable and waits out the coalesce window, then re-checks (the continuation
either fires and enqueues a turn the loop drains, or parks and clears, at which
point the agent is genuinely stable). -
Updated dependencies [
7f367d8]:- create-think@0.1.1