Patch Changes
-
#1395
63cfae6Thanks @threepointone! - Share submit concurrency bookkeeping throughagents/chatand use it from both chat agents.This extracts the
latest/merge/drop/debounceadmission state machine into aSubmitConcurrencyControllerexported fromagents/chat.AIChatAgentsemantics (including merge persistence) are preserved.Thinknow 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:
Thinknow 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
a0a0d17Thanks @threepointone! - think: addbeforeSteplifecycle hook andoutputpassthrough onTurnConfig.beforeStep(ctx)— new lifecycle hook called before each AI SDK step in the agentic loop, wired tostreamText({ prepareStep }). Receives aPrepareStepContext(the AI SDK'sPrepareStepFunctionparameter —steps,stepNumber,model,messages,experimental_context) and may return aStepConfig(PrepareStepResult) to overridemodel,toolChoice,activeTools,system,messages,experimental_context, orproviderOptionsfor the current step. UsebeforeTurnfor turn-wide assembly andbeforeStepwhen the decision depends on the step number or previous step results. Resolves #1363.TurnConfig.output— new optional field onTurnConfigforwarded tostreamText. 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 withactiveTools: []for providers that strip tools whenresponseFormat: "json"is active (e.g.workers-ai-provider). Resolves #1383.- New re-exports from
@cloudflare/think:PrepareStepFunction,PrepareStepResult,PrepareStepContext,StepConfig.
beforeStepis available to subclasses; it is not dispatched to extensions (the AI SDKprepareStepboundary surfaces non-serializable inputs likeLanguageModelinstances). The AI SDK does not exposeoutputormaxStepsper step — set those at the turn level viaTurnConfig. All other extension hook subscriptions are unchanged. -
#1372
040da0fThanks @threepointone! - Remove Think's unused internalsession_idconfig scaffolding and move Think's private config into a dedicatedthink_configtable.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 inthink_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_configwithsession_id = ''are copied intothink_configbefore config reads and writes continue. -
#1396
fdf5a8aThanks @threepointone! - Fix Think persisting a duplicate orphan assistant row when a user submits during a streaming tool turn (#1381).When
useAgentChatposts 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 sametoolCallId. The next turn'sconvertToModelMessagesthen produced a malformed Anthropic prompt and the provider rejected it.reconcileMessagesandresolveToolMergeIdnow live inagents/chatand Think runs them in_handleChatRequestbefore persistence. Staleinput-availablesnapshots pick up the server's tool output viamergeServerToolOutputs, and any incoming assistant whosetoolCallIdalready exists on a server row adopts the server's ID so persistence updates the existing row instead of inserting an orphan.@cloudflare/ai-chatkeeps its existing reconciler behavior; the only change is that it now importsreconcileMessages/resolveToolMergeIdfromagents/chatinstead of a local file. -
#1374
a6e22c3Thanks @threepointone! - Fix stream resumption on page refresh: do not broadcastcf_agent_chat_messagesfrom Think'sonConnectwhile a resumable stream is in flight.Previously, Think unconditionally sent a
cf_agent_chat_messagesframe on every new WebSocket connection. When a client refreshed during an active chat turn, that broadcast arrived in the same connect sequence ascf_agent_stream_resumingand 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_messageson connect when there is no active resumable stream. During an active stream the resume flow is the authoritative source of state:STREAM_RESUMINGtriggers replay of buffered chunks, and the final state broadcast happens when the turn completes. This matches the behavior thatAIChatAgentalready had.Marked the internal
_resumableStreamfield asprotected(previouslyprivate) so framework subclasses and focused tests can coordinate around the resume lifecycle. -
#1384
a7059d4Thanks @threepointone! - IntroduceWorkspaceLike— type thethis.workspacefield as the minimum surface Think actually uses instead of the concreteWorkspaceclass.Think'sworkspaceis now typed asWorkspaceLike(Pick<Workspace, "readFile" | "writeFile" | "readDir" | "rm" | "glob" | "mkdir" | "stat">) rather thanWorkspace.createWorkspaceTools()likewise accepts anyWorkspaceLike. The default runtime value is unchanged — a fullWorkspacebacked 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
workspacewith 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. Seeexamples/assistantfor the cross-chat shared workspace built on this.Consumers who use
createWorkspaceStateBackend(workspace)from@cloudflare/shell(codemode'sstate.*API) still need a concreteWorkspace— that helper reaches for more of the filesystem surface thanWorkspaceLikecovers.