feat(think): Think API strategy — turns, actions, and channels (+ shared chat refactors)#1790
Merged
Conversation
Extract the event-driven auto-continuation barrier (tool-result -> auto-continue, #1649 / #1650) — previously duplicated line-for-line across @cloudflare/ai-chat and @cloudflare/think — into a single AutoContinuationController in agents/chat. The controller owns the coalesce timer, the _barrierActive double-fire guard, and the full schedule -> coalesce -> fire lifecycle (schedule / rearmForBatch / armTimer / fireWhenStable / activateDeferredAndReschedule / cancelTimer / isArmed / reset). It is parameterized over a small AutoContinuationHost interface (the stream-active signal, incomplete-batch / pending-interaction predicates, the apply-drain primitive, keepAliveWhile, and each host's continuation-turn fire pipeline) plus a ContinuationSpec for the per-schedule context. COALESCE_MS (50ms) is single-sourced here. Exported @internal for the sibling host packages — not public API. Includes a focused unit suite (17 tests) covering the scheduling branches, coalescing, the double-fire guard, the stream-active gate, the incomplete-batch hold, the drain orchestration, deferred activation, reset(), and the reset-during-drain reentrancy isolation that matters under hibernation/eviction. Co-authored-by: Cursor <cursoragent@cursor.com>
…Think idle Wire both hosts onto the shared AutoContinuationController. Each host keeps its original method names as thin delegating wrappers (_scheduleAutoContinuation, _rearmPendingAutoContinuationForBatch, _onStreamingTurnFinalized, _activateDeferred..., _fireAutoContinuation), so every call site is untouched. _fireAutoContinuation is now parameterless and reads connection/requestId from _continuation.pending; _resetAutoContinuationTimer and _fireAutoContinuationWhenStable are absorbed into the controller; ai-chat's duplicate AUTO_CONTINUATION_COALESCE_MS is removed in favor of AutoContinuationController.COALESCE_MS. Net ~-375 lines across the two hosts with no observable behavior change (pure de-dup; covered by the agents changeset). White-box test probes are repointed at the relocated fields (_autoContinuation._timer / ._barrierActive); no host-suite spec edits were required, which was the primary parity gate. Fast-follow (intentional, @cloudflare/think patch): Think.waitUntilStable() now consults the controller's isArmed() via a new _hasArmedContinuation() so it waits out an armed-but-unfired auto-continuation before reporting stable — converging Think's idle definition onto ai-chat's waitForIdle(). Both Think callers are recovery paths bounded by stableTimeoutMs, so it cannot wedge. Adds a focused test asserting waitUntilStable holds while a continuation is armed (#1650). Co-authored-by: Cursor <cursoragent@cursor.com>
…progress log - rfc-chat-recovery-foundation.md: mark the AutoContinuationController follow-up done with as-built notes (controller location/shape, parameterless fire, COALESCE_MS single-sourcing, the Think waitUntilStable convergence fast-follow, and the validation/Bugbot results); move it out of the deferred list, leaving the adapter-spine helpers as the remaining host-convergence extraction. Also archives the ~2200-line inline Progress log to a sibling file now that the branch is frozen, per design/AGENTS.md. - rfc-chat-recovery-foundation-progress.md: the archived Progress log with a back-link to the RFC. - chat-shared-layer.md: add auto-continuation-controller.ts to the architecture map and a History entry describing the extraction + Think idle convergence. Co-authored-by: Cursor <cursoragent@cursor.com>
Extract three byte-identical fragments duplicated across @cloudflare/ai-chat and @cloudflare/think into agents/chat as @internal primitives: - async-helpers.ts: TIMED_OUT, awaitWithDeadline (deadline-bounded race), and drainInteractionApplies (substrate-free interaction-apply completeness drain, parameterized by hasPending / getTail). - classifyAgentToolChildRecovery(storage) in recovery-incident.ts (in-progress > failed > none precedence). - interceptAgentToolBroadcast(msg, hooks) in agent-tools.ts (the #1575 agent-tool tailing snoop), parameterized by an AgentToolBroadcastHooks substrate (forwarder/live-sequence/last-error maps, response-frame type, run-lookup). Adds focused unit coverage (async-helpers deadline + drain, classify precedence, broadcast forward/error/passthrough/no-op) and an agents patch changeset. Co-authored-by: Cursor <cursoragent@cursor.com>
Repoint both hosts' _awaitWithDeadline / _drainInteractionApplies / _classifyAgentToolChildRecovery to one-line delegates over the shared agents/chat helpers, drop the duplicate TIMED_OUT symbol, and route each broadcast() override through interceptAgentToolBroadcast — behind a cheap size-guard so the common no-child path stays allocation-free — before super.broadcast. Pure internal de-dup, no behavior or API change: host suites pass with zero spec edits. Net ~-160 lines across the two hosts. Co-authored-by: Cursor <cursoragent@cursor.com>
Record the extraction in chat-shared-layer.md (module map + History) and add the as-built tracked follow-up to rfc-chat-recovery-foundation.md, including the deliberately-deferred bucket-3 dispatch/classify/terminalize methods and the verified "recovering-cleared-on-normal-completion" residual. Co-authored-by: Cursor <cursoragent@cursor.com>
Expose a mode-keyed runTurn API over the existing Think turn entry points so callers can use one additive surface before the TurnSpec admission refactor lands. Co-authored-by: Cursor <cursoragent@cursor.com>
Unify Think turn admission behind an internal TurnSpec spine while preserving path-specific execution order, and guard nested blocking admissions with async-local turn context instead of allowing queue deadlocks. Co-authored-by: Cursor <cursoragent@cursor.com>
Add a minimal chat:turn start/finish event pair around admitted queue execution so observers can correlate Think turn lifecycles without path-specific noise. Co-authored-by: Cursor <cursoragent@cursor.com>
Record the shipped chat:turn:start/finish event contract so the Turns RFC no longer describes the earlier per-status event proposal. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Compile action approval policies onto the existing approval flow and persist stable descriptors for UI surfaces. Co-authored-by: Cursor <cursoragent@cursor.com>
Add permission metadata and default-full-grant authorization hooks so actions can deny before approval or execution. Co-authored-by: Cursor <cursoragent@cursor.com>
Preserve approved action inputs across continuation and cover function-valued policies plus authorization rechecks. Co-authored-by: Cursor <cursoragent@cursor.com>
Persist settled action outputs behind stable idempotency keys so recovery can replay side effects without re-executing actions. Co-authored-by: Cursor <cursoragent@cursor.com>
Add durable-pause action descriptors and resume support alongside the attachReply reply-attachment side channel so action approvals and delivery hints survive real turn lifecycles with focused regression coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
Allow stale pending action ledger rows with explicit idempotency keys to be reclaimed after a lease window, while preserving conservative fallback-key behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
Add a public channel surface over the existing messenger runtime: configureChannels()/ChannelDefinition (web, voice, messenger, custom) wrapping getMessengers(), a no-turn deliverNotice() with informModel, additive DeliveryTag (kind + turnEnded) on messenger snapshots, per-channel policy (instructions, tool-narrowing, maxTurns) applied as overridable defaults, turn-scoped channel context threaded through runTurn and persisted for recovery, reply-attachment rendering at delivery, and channel:*/notice:* observability events. Reconcile the sibling turns/voice/actions RFCs with what shipped and track deferred follow-ups (sub-agent channels, DeliveryKind on the delivery wire, bundled-adapter fetchThread, attachment ordering). Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve out-of-turn deliverNotice() to messenger channels via the chat
SDK's chat.thread(id) primitive (infers the adapter from the thread-id
prefix), so it works for every adapter with no per-adapter fetchThread
wiring; still requires { thread }.
Consolidate the Channels v1 follow-ups (sub-agent channels, DeliveryKind
on the delivery wire, attachment ordering, voice spike) plus the recovery
convergence into a single tracker in the Channels RFC, and add a
recovery-progress breadcrumb noting per-channel policy depends on the
persisted metadata.channel so the recovery-engine convergence preserves it.
Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: ede4476 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
agents
@cloudflare/ai-chat
@cloudflare/codemode
create-think
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
…el; guard web override
- Add an `agents:channel` diagnostics channel and route channel:resolved,
channel:delivered, notice:delivered, and notice:failed there instead of the
catch-all lifecycle channel; add a typed ChannelEventMap bucket so they are
reachable via subscribe("channel", cb).
- Reserve the implicit "web" channel for the built-in WebSocket surface:
configureChannels() may override its policy with a { kind: "web" } entry but
throws if it tries to replace web with another kind.
Co-authored-by: Cursor <cursoragent@cursor.com>
A configureChannels({ web: { kind: "web", instructions } }) override previously
replaced the implicit web channel wholesale, silently dropping its built-in
capabilities (canStream / canEditMessages). Merge the override over the implicit
web defaults so policy-only overrides retain capabilities and ingress.
Co-authored-by: Cursor <cursoragent@cursor.com>
The configureChannels() path rejected replacing the reserved "web" channel, but
the getMessengers() loop only checked for duplicates against configureChannels()
— so getMessengers() returning { web: ... } silently overwrote the implicit web
channel with a kind: "messenger" one, breaking native WebSocket chat. Apply the
same reservation guard symmetrically.
Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lands the Think API strategy trio (turns → actions → channels) on top of two shared
agents/chatrefactors. The work is sequential — each layer consumes seams from the one below — so it's reviewed here as one coherent stack (20 commits, 21 changesets).1. Shared chat primitives (
agents/chat)AutoContinuationControllerextracted and adopted by both Think and AIChatAgent; Think's idle/auto-continuation converged onto it.agents/chatand delegated to from both chat hosts.2. Turns (
runTurn/_admitTurn)runTurnfacade over the existing turn-entry paths; all turns routed through a single_admitTurnadmission spine.chat:turn:*admitted-turn observability events.3. Actions (descriptors → ledger → approvals)
authorizeTurnauthorization hooks, durable-pause approval descriptors, andctx.attachReply()reply attachments.action:*ledger/pause/reply observability events.4. Channels + notices
configureChannels()/ChannelDefinition(web, voice, messenger, custom) wrappinggetMessengers().deliverNotice()withinformModel; additiveDeliveryTag(kind+turnEnded); per-channel policy (instructions / tool-narrowing /maxTurns) as overridable defaults; turn-scoped channel context threaded throughrunTurnand persisted (metadata.channel) for recovery; reply-attachment rendering;channel:*/notice:*observability.chat.thread(id)primitive (works for every adapter).Docs: reconciled the sibling turns/voice/actions RFCs with what shipped, added a consolidated Channels v1 follow-up tracker, and left a recovery-progress breadcrumb noting per-channel policy depends on persisted
metadata.channel.Test plan
pnpm run checkgreen (sherif, export checks, oxfmt, oxlint, typecheck across 113 projects)deliver-notice,channel-threading,channel-policy,attachment-consumption,channelsmessengers, messenger recovery)packages/think/src/testsandpackages/agents/src/chat/__tests__Notes for reviewers
DeliveryKindon the delivery wire, attachment ordering, voice transport spike) and the headline next effort (Turns recovery-engine convergence) are tracked indesign/rfc-think-channels.md→ "Follow-up tracker".@cloudflare/thinkminor;agentspatches for the observability unions).Made with Cursor