github cloudflare/agents @cloudflare/think@0.4.1

12 hours ago

Patch Changes

  • #1395 63cfae6 Thanks @threepointone! - Share submit concurrency bookkeeping through agents/chat and use it from both chat agents.

    This extracts the latest/merge/drop/debounce admission state machine into a SubmitConcurrencyController exported from agents/chat. AIChatAgent semantics (including merge persistence) are preserved. Think now picks up the same pending-enqueue protection, so an overlapping submit is still detected while an accepted request is between admission and turn queue registration.

    Additional fixes:

    • Think now captures the turn generation immediately after admission and threads it into _turnQueue.enqueue, so a clear that lands between admission and queue registration cannot run a stale turn.
    • Pending-enqueue tracking is now bound to a release function tied to the controller's reset epoch, so a release from a pre-reset submit can no longer erase a post-reset submit's marker and let a third submit slip through as non-overlapping.
    • Debounce cancellation correctly resolves all in-flight waiters instead of overwriting a single timer slot.
  • #1394 a0a0d17 Thanks @threepointone! - think: add beforeStep lifecycle hook and output passthrough on TurnConfig.

    • beforeStep(ctx) — new lifecycle hook called before each AI SDK step in the agentic loop, wired to streamText({ prepareStep }). Receives a PrepareStepContext (the AI SDK's PrepareStepFunction parameter — steps, stepNumber, model, messages, experimental_context) and may return a StepConfig (PrepareStepResult) to override model, toolChoice, activeTools, system, messages, experimental_context, or providerOptions for the current step. Use beforeTurn for turn-wide assembly and beforeStep when the decision depends on the step number or previous step results. Resolves #1363.
    • TurnConfig.output — new optional field on TurnConfig forwarded to streamText. Accepts the AI SDK's structured-output spec (e.g. Output.object({ schema }), Output.text()) so a single agent can keep tools enabled on intermediate turns and return schema-validated structured output on a designated turn — without losing tools at model construction. Combine with activeTools: [] for providers that strip tools when responseFormat: "json" is active (e.g. workers-ai-provider). Resolves #1383.
    • New re-exports from @cloudflare/think: PrepareStepFunction, PrepareStepResult, PrepareStepContext, StepConfig.

    beforeStep is available to subclasses; it is not dispatched to extensions (the AI SDK prepareStep boundary surfaces non-serializable inputs like LanguageModel instances). The AI SDK does not expose output or maxSteps per step — set those at the turn level via TurnConfig. All other extension hook subscriptions are unchanged.

  • #1372 040da0f Thanks @threepointone! - Remove Think's unused internal session_id config scaffolding and move Think's private config into a dedicated think_config table.

    Older builds wrote Think-owned config into Session's shared assistant_config(session_id, key, value) table even though Think never actually had top-level multi-session support and _sessionId() always returned the empty string. Think now stores its private config rows in think_config(key, value), which better matches the shipped model of one Think Durable Object per conversation and avoids overloading Session's shared metadata table.

    Existing Durable Objects are migrated automatically on startup: legacy Think-owned keys stored in assistant_config with session_id = '' are copied into think_config before config reads and writes continue.

  • #1396 fdf5a8a Thanks @threepointone! - Fix Think persisting a duplicate orphan assistant row when a user submits during a streaming tool turn (#1381).

    When useAgentChat posts an in-flight assistant snapshot it minted optimistically (client-generated ID, state: "input-available"), Session's INSERT-OR-IGNORE-by-ID would store it as a separate row alongside the eventual server-owned assistant for the same toolCallId. The next turn's convertToModelMessages then produced a malformed Anthropic prompt and the provider rejected it.

    reconcileMessages and resolveToolMergeId now live in agents/chat and Think runs them in _handleChatRequest before persistence. Stale input-available snapshots pick up the server's tool output via mergeServerToolOutputs, and any incoming assistant whose toolCallId already exists on a server row adopts the server's ID so persistence updates the existing row instead of inserting an orphan.

    @cloudflare/ai-chat keeps its existing reconciler behavior; the only change is that it now imports reconcileMessages / resolveToolMergeId from agents/chat instead of a local file.

  • #1374 a6e22c3 Thanks @threepointone! - Fix stream resumption on page refresh: do not broadcast cf_agent_chat_messages from Think's onConnect while a resumable stream is in flight.

    Previously, Think unconditionally sent a cf_agent_chat_messages frame on every new WebSocket connection. When a client refreshed during an active chat turn, that broadcast arrived in the same connect sequence as cf_agent_stream_resuming and overwrote the in-progress assistant message the client was about to rebuild from the resumed stream. The assistant reply would stay hidden until the server finished the turn and re-broadcast the persisted history.

    Now Think only broadcasts cf_agent_chat_messages on connect when there is no active resumable stream. During an active stream the resume flow is the authoritative source of state: STREAM_RESUMING triggers replay of buffered chunks, and the final state broadcast happens when the turn completes. This matches the behavior that AIChatAgent already had.

    Marked the internal _resumableStream field as protected (previously private) so framework subclasses and focused tests can coordinate around the resume lifecycle.

  • #1384 a7059d4 Thanks @threepointone! - Introduce WorkspaceLike — type the this.workspace field as the minimum surface Think actually uses instead of the concrete Workspace class.

    Think's workspace is now typed as WorkspaceLike (Pick<Workspace, "readFile" | "writeFile" | "readDir" | "rm" | "glob" | "mkdir" | "stat">) rather than Workspace. createWorkspaceTools() likewise accepts any WorkspaceLike. The default runtime value is unchanged — a full Workspace backed by the DO's SQLite — so the vast majority of consumers need no changes.

    This unlocks patterns like a shared workspace across multiple agents: a child agent can override workspace with a proxy that forwards each call to a parent DO via RPC, and the rest of Think's workspace-aware code (the builtin tools, lifecycle hooks) keeps working without cast gymnastics. See examples/assistant for the cross-chat shared workspace built on this.

    Consumers who use createWorkspaceStateBackend(workspace) from @cloudflare/shell (codemode's state.* API) still need a concrete Workspace — that helper reaches for more of the filesystem surface than WorkspaceLike covers.

Don't miss a new agents release

NewReleases is sending notifications on new releases.