Skip to content

feat(think): Think API strategy — turns, actions, and channels (+ shared chat refactors)#1790

Merged
threepointone merged 23 commits into
mainfrom
task2-adapter-spine-helpers
Jun 22, 2026
Merged

feat(think): Think API strategy — turns, actions, and channels (+ shared chat refactors)#1790
threepointone merged 23 commits into
mainfrom
task2-adapter-spine-helpers

Conversation

@threepointone

@threepointone threepointone commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Lands the Think API strategy trio (turns → actions → channels) on top of two shared agents/chat refactors. 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)

  • AutoContinuationController extracted and adopted by both Think and AIChatAgent; Think's idle/auto-continuation converged onto it.
  • Shared adapter-spine helpers factored into agents/chat and delegated to from both chat hosts.

2. Turns (runTurn / _admitTurn)

  • runTurn facade over the existing turn-entry paths; all turns routed through a single _admitTurn admission spine.
  • chat:turn:* admitted-turn observability events.

3. Actions (descriptors → ledger → approvals)

  • Action descriptors, durable action ledger (with pending-retry leases and reclaim/sweep), authorizeTurn authorization hooks, durable-pause approval descriptors, and ctx.attachReply() reply attachments.
  • action:* ledger/pause/reply observability events.

4. Channels + notices

  • Generalizes the messenger runtime into a public channel surface: configureChannels() / ChannelDefinition (web, voice, messenger, custom) wrapping getMessengers().
  • No-turn deliverNotice() with informModel; additive DeliveryTag (kind + turnEnded); per-channel policy (instructions / tool-narrowing / maxTurns) as overridable defaults; turn-scoped channel context threaded through runTurn and persisted (metadata.channel) for recovery; reply-attachment rendering; channel:* / notice:* observability.
  • Out-of-turn messenger notices resolve via the chat SDK's 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 check green (sherif, export checks, oxfmt, oxlint, typecheck across 113 projects)
  • New suites pass: deliver-notice, channel-threading, channel-policy, attachment-consumption, channels
  • Messenger parity suites unchanged (messengers, messenger recovery)
  • Turn/action/adapter-spine suites under packages/think/src/tests and packages/agents/src/chat/__tests__
  • CI green on PR

Notes for reviewers

  • Deferred follow-ups (sub-agent channels, DeliveryKind on the delivery wire, attachment ordering, voice transport spike) and the headline next effort (Turns recovery-engine convergence) are tracked in design/rfc-think-channels.md → "Follow-up tracker".
  • 21 changesets included (@cloudflare/think minor; agents patches for the observability unions).

Made with Cursor


Open in Devin Review

threepointone and others added 20 commits June 20, 2026 20:18
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-bot

changeset-bot Bot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ede4476

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agents Minor
@cloudflare/think Minor

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

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Open in Devin Review

Comment thread packages/agents/src/observability/index.ts
Comment thread packages/think/src/channels/index.ts
@pkg-pr-new

pkg-pr-new Bot commented Jun 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@1790

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@1790

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@1790

create-think

npm i https://pkg.pr.new/create-think@1790

hono-agents

npm i https://pkg.pr.new/hono-agents@1790

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@1790

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@1790

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@1790

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@1790

commit: ede4476

threepointone and others added 3 commits June 22, 2026 01:56
…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>
@threepointone threepointone merged commit 190ea81 into main Jun 22, 2026
7 checks passed
@threepointone threepointone deleted the task2-adapter-spine-helpers branch June 22, 2026 01:24
@github-actions github-actions Bot mentioned this pull request Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant