Repository navigation
@tanstack/ai@1.0.0
Major Changes
- #1840
f1b3f65- TanStack AI 1.0.0. This release removes every API that was deprecated in 0.x. Read the upgrade guide before you upgrade: https://tanstack.com/ai/latest/docs/migration/v1- Removed all
@deprecatedexports, options, fields, and type aliases. Each one has a replacement that already shipped in 0.x. - Removed
addToolApprovalResponse()fromChatClientand every chat hook. Resolve the boundtool-approvalinterrupt withresolveInterruptinstead. The devtools eventtools:approval:respondedis also removed. - Removed the old wire formats: the CUSTOM
approval-requestedandtool-input-availableevents, the[DONE]SSE line, thedatacopy offorwardedProps, pre-binding interrupt descriptors and theirapproval_requiredandclient_tool_inputreasons, and the STEP events that adapters sent next toREASONING_*events. Use a 1.0 client with a 1.0 server. - Removed the
toolNamealias fromTOOL_CALL_STARTchunks. ReadtoolCallNameinstead. threadIdis now required onuseChatand every other chat hook,ChatClient, the generation hooks and clients,chat(),summarize(), the generation functions,codeModeWithSnippets(),selectRelevantSnippets(),CompactionContext, andSummarizeInput. TanStack AI no longer makes a thread id for you. Pass a stable id for each conversation.createChatHookfactory options no longer takethreadId: pass it on each hook call. Octane generation hooks takethreadIdinstead ofid.@tanstack/ai-sandboxremovesresolveDurableThreadIdandDurableThreadIdRequiredError.- The published
@tanstack/ai-octane/uitypes are now strict.UI.Messages,UI.Message,UI.Part, andUI.Interruptstype their render children.ToolPropstypespart.inputandpart.outputfor the named tool.toolsComponentsandinterruptsComponentsare required when the chat options declare tools or named interrupts. Code that relied on the old loose types can fail to compile. - A connection adapter's
connect()andsend()need the run context (withthreadId) as their fourth argument.ChatClientand the generation clients pass it. @tanstack/ai-sandbox:createToolBridgeCore(),startHostToolBridge(), aToolBridgeProvisioner, andexecuteHostTool()take a requiredthreadId. Bridged tools get it in their context.@tanstack/ai-sandbox: the harness tool bridge andexecuteHostTool()refuse a tool withneedsApproval. Before, the tool ran with no approval. RemoveneedsApproval, or run the tool with an HTTP adapter.stream()andrpcStream()pass the AG-UIRunAgentInputfor the run as the fourth factory argument, the same body the HTTP adapters send. Read it on the server withchatParamsFromRequestBody()orgenerationParamsFromBody(), sothreadId,runId, and interruptresumereach a server function.RunAgentInputis exported from@tanstack/ai/client.- A generation
fetchergetsthreadIdandrunIdin its second argument, next tosignalandheaders. Pass them to the server generation call. generationParamsFromBody()andgenerationParamsFromRequest()read the generation input fromforwardedProps. A server that readsbody.datagetsundefined. Readbody.forwardedPropsinstead. They also requirethreadIdandrunIdand return them as strings, the same aschatParamsFromRequestBody()and the AG-UIRunAgentInput. A body that holds only the input is rejected.- Removed the readers for data saved in old 0.x formats (old Cloudflare run-log records, a bare
UIMessage[]from client persistence, artifact records withoutblobKey). - Removed provider models that are shut down.
@tanstack/ai-react-ui,@tanstack/ai-solid-ui, and@tanstack/ai-vue-uiget no 1.0 release. Import from@tanstack/ai-react/ui,@tanstack/ai-solid/ui, or@tanstack/ai-vue/ui.
- Removed all
Minor Changes
-
#1743
959f43e- AddEvaluateError, a typed error an evaluate adapter throws when the provider answered but the call cannot become a result.@tanstack/aiand@tanstack/ai/adaptersexport the class, theisEvaluateError()type guard, theEvaluateErrorInittype andEVALUATE_REFUSAL_CODE. The error keeps what the provider sent back:code: a stable code to branch on. A refused question usesEVALUATE_REFUSAL_CODE('refusal').usage: the tokens the call used.model: the model that answered.answers: the raw wire answers that came back, beforedecide()maps them. Achoice()answer haschoice(for example{ type: 'choice', choice: 'billing', ... }), and aboolean()answer is{ type: 'noul', noul: 0.9 }, not{ value }. The refused question has no key.
When OpenAI refuses to answer a question,
decide()now throws anEvaluateErrorwithcode: 'refusal'instead of a plainError. If more than one question is refused, the message names the first one.Use
isEvaluateError(error)to narrow a caught error. It checksnameandcode, notinstanceof, so it still works when the app installs more than one copy of@tanstack/ai.When an adapter throws an
EvaluateErrorthat carriesusage,decide()reports that usage through theevaluate:usageevent and the middlewareonUsagehook, then runsonErrorand rethrows the same error.onFinishdoes not run.try { await decide({ adapter, state, questions }) } catch (error) { if (isEvaluateError(error) && error.code === EVALUATE_REFUSAL_CODE) { console.log(error.usage, error.answers) } throw error }
The message text does not change. The error is still an
Error, but itsnameis now'EvaluateError', so logs and the OpenTelemetryerror.typeshow that name instead of'Error'. -
#1806
468063c- FixwithPersistencefailing to resume an interrupt of a subagent tool call withTool <name> is unavailable for interrupt <id>.The parent run now leaves interrupts its child answers to the child, using the newctx.childResumeIdson the middleware context, and still commits them once the run succeeds.With
withPersistence, the resume of a child is server-authoritative:- The stored interrupt records decide which interrupts a child owns. A resume whose subagent cards claim other interrupts fails with
InterruptResumeValidationError. - The child resumes from its stored messages, not from the card the client sent back.
LoadChildresults get an optionalpendingfield for this. A middleware that providesloadChildmust setpendingfor a waiting child, or the agent tool does not resume it.
- The stored interrupt records decide which interrupts a child owns. A resume whose subagent cards claim other interrupts fails with
-
#1880
781a3a8- Put the provider error type and the request id onRUN_ERROR. The Anthropic adapter and the adapters built on@tanstack/openai-baseseterrorType(for exampleoverloaded_error) andrequestId(from the SDK error or therequest-id/x-request-idheader).chat()moves both tometadata.tanstack, so the client gets them too.codestays the HTTP status. Adapter authors can read both values withtoRunErrorDetailsfrom@tanstack/ai/adapter-internals. -
#1883
39abb7e- Core support for the harness: subagent binding options, theonGuardToolCallhook, server-side BYOK keys, harness tool runs, checked streamedoutputSchemaresults, and resume fixes.Add the harness asks from the fifth findings list.
chat({ outputSchema, stream: true })now checks the full result against a Standard Schema. A failure ends withRUN_ERRORstructured-output-validation-failed, with the model text inmetadata.tanstack.rawText.session.send(prompt, { outputSchema })gives the turn a typedstructuredvalue. It is per send only, and the HTTP handler does not take it.- A server tool reads run capabilities with
context.get(capability)andcontext.getOptional(capability), for example the sandbox underwithSandbox. - Readers keep the full history after a durable compaction.
session.transcript(),GET transcript, and the chat hydration show every message. The model still gets the compacted context. createHarnessHost({ acceptInput })sees each input after the access check and before the log keeps it. It can change the input or refuse it.- A plugin
adapter()pick withoverride: truewins over the stored thread model. onModelErrorcan return{ retry: true, adapter }to retry on another model.ModelErrorContexthasadapterandmodel.- Middleware reads
ctx.context.run.sourceandlabelto tell a turn call from a plugin, router, or transfer call. host.sessions.archive()andunarchive(). Lists hide archived threads unless you passwithArchived: true.host.sessions.eraseUser({ harness, threadId, userId, replies })replaces one person's messages with[removed], deletes their media, and cuts the log before a new snapshot. The neweraseaccess action guards it.HarnessRecordis exported, for a typedLogStorewrapper.- Coding-tool file calls pass their abort signal through
WorkspaceBackendto the sandbox. withCompactionlimits (maxTokens,contextWindow,background.atTokens) can be a function of{ model }.
Run a harness tool with no model turn, add host records from a client, and add a per-run system preamble.
session.runTool({ name, args?, meta?, inputId?, user? })runs one harness tool with no model
turn, for example from a schedule or a webhook. The call goes through the
same middleware, input check, andpermissions()rules as a model call, and
the call and its result join the transcript. A tool that asks for approval
stops the turn with an interrupt. While interrupts wait, the call is refused
withpending_interrupts.metais echoed as atanstack.injectioncustom
event.argsis typed from the input schema of a harness tool.expose.toolslists the harness tools a client may run with a
{ op: 'tool', name, args?, meta? }input, or with the new
client.runTool({ name, args?, meta?, inputId? }). The list is typed from the harness
tools, anddefineHarnessthrows for a name that is not intools. Any
other tool is refused withnot_exposed. The capabilities document marks
each tool withexposed.expose.recordslists the host record types a client may add with an
{ op: 'append', records }input, or with the newclient.appendRecords({ records }).
A record of another type, or with athreadfield, is refused with
not_exposed. The session log keeps the records.createHarnessMcpServeradds atool_<name>tool for each tool in
expose.tools, and anappendtool whenexpose.recordshas types. A tool
input with its ownthreadIdfield throws when the server is created,
because MCP usesthreadIdto pick the thread.session.send(message, { systemPreamble })prepends per-run, server-only
system prompts ahead of the harness's own system prompts (for example
host-provided memory). A client input cannot set them.- A tool call in a harness turn gets
threadIdandrunIdin its execution
context.ToolExecutionContextin@tanstack/aihas the two new optional
fields for a host to set.
A host can add fields to the
ctxan agent'srungets.subagents.binding.extend(ctx)returns the fields to add. A child'sctx.chat({ subagents })passes it down, so the fields reach nested children too.SubagentContextExtensionis an empty interface thatSubagentRunContextincludes. A package augments it to type its fields, for examplectx.harnessfrom@tanstack/ai-harness. Make each field optional, because a run that a plainchat()starts has no host.
Add
interruptstoSubagentBinding.- A host sets
binding.interruptsto give interrupt definitions to everyctx.chatcall of an agent run. Then the host middleware inchatMiddlewarecan stop the child with these interrupts. - The own
interruptsof the call win. When a host definition has the same id as one of the call, the host definition is not added. - A child's
ctx.chat({ subagents })passes the definitions on to its own children. - A plain
chat()call does not get them.
Add
lastChatMiddlewaretoSubagentBinding. It runs after the own middleware of eachctx.chatcall of an agent run, so it sees the final model context and tool results. A child'sctx.chat({ subagents })passes it on, after the child's own.Add
validateJsonSchema(schema, value). It checks a value against a JSON Schema with@cfworker/json-schema, as a tool input is checked, but with no repair. The result has the Standard Schema shape:{ value }or{ issues }. The root also exports theStandardSchemaV1andStandardJSONSchemaV1types.An agent
runthat throws the shape ofMCPInputRequiredError(name,kind,request) stops the child assuspended, as a tool does, and the parent run stops with its interrupt. The interrupt has a generic binding and no tool call. Its id is theinterruptIdof the thrown value, elsemcp_input_<subagentRunId>, and its message is the error message. The resume runs the child again, andctx.resumehas the answer.Add
SubagentBinding.debug, thedebugoption for the model calls of child agents that set nodebugof their own.Add the
onGuardToolCallmiddleware hook for checks that must always run, such as permissions.- It runs for every tool call, after all
onBeforeToolCallhooks and after theinputSchemacheck.hookCtx.argsholds the final checked arguments that the tool gets. - The guard of every middleware runs, in order, until one returns a decision. An earlier
onBeforeToolCalldecision does not skip it. - Return
{ type: 'skip', result }to deny the call. The tool does not run, and the model getsresult, the same as anonBeforeToolCallskip. Return{ type: 'abort', reason }to abort the run. - It runs before a server tool runs and before a client tool goes to the client. A guard cannot change the arguments.
- The new
GuardToolCallDecisiontype is exported.
BYOK keys can now live on the server.
@tanstack/ai/byokexports theCredentialStorecontract, theCredentialandCredentialScopetypes, anddefineCredentialStore. ACredentialScopehastenantIdanduserId. ItsthreadIdis optional, and a store ignores it.@tanstack/ai/byok/serveradds:storedProviderKeys(store, scope): provider keys from theapi_keycredentials of a user, then of the tenant, then the provider'senvnames. Passkeys.adapter(...)tochat()andkeys.gettosubagents.keys.readCredential(store, scope, id): one credential, the user's own first, then the tenant's. A user with no tenant reads only their own.tenantScopeOf(scope): the tenant scope of a user scope, orundefinedwhen the scope has no user or no tenant.memoryCredentialStore(): a store in memory, for development and tests.envKeyEntry(provider): the firstprovider.envname that is set, with its value.readCredentialEntry(store, scope, id): reads a credential likereadCredential. It also tells you which scope had the credential (the user or the tenant), and gives its revision when the store hasgetVersioned.
CredentialStorehas two optional methods for compare-and-set writes,getVersionedandsetIf, with the same shape as the pair onMetadataStore. A store without them still works.memoryCredentialStorehas both.Keep the store in your own database or in a secrets manager, and encrypt the secrets at rest. An
oauthcredential can keep the OAuthclientit was issued to.listnever returns secret values.scrubSecretsfrom@tanstack/ai/byoknow also removes the JSON-escaped and URL-encoded forms of each secret. It skips a secret that is shorter than 6 characters, so a short value does not remove common words.Credentialhas a new type,secret_answer({ value, expiresAt }). The harness keeps a secret answer to a question there for a short time and deletes it once read. Leave this type out of a list of connected accounts.memoryCredentialStore().listreturns itsexpiresAt.Approved edits to tool arguments stay raw until the final input check.
@tanstack/ai: a resume no longer rejects approvededitedArgsthat do not fit the tool'sinputSchema. The edit goes throughonBeforeToolCalland the final input check, like the model's arguments. If it still does not fit, the approval is used, the tool does not run, and the model gets the input error as the tool result. The transcript keeps the approved edit as it was sent, but only when the call passed the check.@tanstack/ai-persistence:withPersistencebinds the client-tool phase of an approved tool call to its approval. After a restart, the client result resumes with the approval that came before it, and a stale or missing approval fails the resume.
A nested subagent that waits on an interrupt now continues after the resume under
withPersistence. Before, it ran again from its prompt. Core rebuilds the card of the nested child from the store and takes only its id from the client card.LoadChildtakes an optionalparentSubagentRunId, and returnsinterruptIdsandparentToolCallId.chat()asks the middlewareonShouldContinueafter it runs the pending tool calls of a resume, before the next model call. A middleware that returnsfalsethere ends the run with no more model calls. The loop strategy is not asked at that point.WebSocketLikehas an optionalbufferedAmount. A server can read it to stop sending to a slow socket. A socket without it works as before.
Patch Changes
-
#1846
fcbd199- ApplyACTIVITY_SNAPSHOTandACTIVITY_DELTAevents that chat middleware adds toctx.activities, sowithPersistencesaves them andreconstructChatrestores their activity rows. A middleware that passes on an adapter activity event, or an exact copy of it, does not apply it twice. -
#1755
39c1c79- Fix recovery, cancel, steer, and provider bugs, and fill small harness gaps.- Recovery: a resolve that another host recovers runs its continuation. Before, it settled
completedwith no answer. - Approvals: a tool that a middleware adds in
onConfigcan now be approved.chat()checks a durable resume after the wholeonConfigchain. - Cancel: a cancelled turn keeps the text it streamed, and ends with
TEXT_MESSAGE_ENDandRUN_FINISHED(outcome: cancelled). A turn that times out ends withRUN_ERROR(code: 'timeout'). - Steer: a steer from another sender gets a
queuedreceipt with its own operation.host.events()showsidleafter a turn that a steer joined. - The duplicate-input check no longer depends on the key order of the stored input.
TurnInfohasmodel,attempt, andsender. The log keeps the sender'sname.harness.input.acceptedcarries the message. The chat hydration haspendingWaits.ObservedSession.settled()reads a settlement with no open session. Another host'ssnapshot().pluginsshows the saved plugin state.- New options:
subagents.background: false,media.urlTtlMs,conversationSummarizer({ middleware }), and thebeforeWriteworkspace hook, which can refuse a write. - Plugin model calls (
ctx.model.chat) run the harness middleware. - Exports:
SHUTDOWN_REASON,BUDGET_REASON, andRUN_CANCEL_REASONfrom@tanstack/ai-harness,HARNESS_EVENTSfrom@tanstack/ai-harness/client, andChatHydrateOptionsandChatHydrationResultfrom@tanstack/ai-client. - Compaction drops a background summary that another model prepared.
- Sandbox file calls take an optional
{ signal }. - Anthropic:
outputSchemamoves keywords that Anthropic rejects intodescription, usage reports 1-hour cache writes ascacheWrite1hTokensagain, andreasoningon a model with no reasoning entry warns once. - OpenAI Responses: a message whose model call sent a reasoning item is marked with
metadata.tanstack.reasoningItem. A replay of that message without its reasoning item no longer sends itsmsg_andfc_ids. - Cloudflare: a
data:line with nochoicesno longer throws, and the SSE fix-up acceptsdata:with no space, CRLF line ends, and any case in the content type. - Structured output: a schema failure carries
rawText, and a parse failure has the codestructured-output-parse-failed. ChatClient.addToolResultanswers the interrupt when the tool's schemas differ, so the result is sent.- The persistence conformance suite no longer fails a correct store on a slow runner.
- Recovery: a resolve that another host recovers runs its continuation. Before, it settled
-
#1869
fa2a291- Accept a missingmidConversationChannelsvalue on text adapters whenexactOptionalPropertyTypesis on.chat()can takefakeText()and the provider text adapters again. -
#1851
f7e7cce- Add@tanstack/ai-neonfor the Neon AI Gateway:neonTextsends Claude to the Anthropic Messages route, codex and*-promodels to the Responses route, and every other model to Chat Completions, withneonEmbeddingandneonSummarize.@tanstack/openai-basegets astreamOptions(model)hook, so a Chat Completions backend that rejectsstream_optionscan leave it out. -
#1805
7a0a48b- Middleware can readctx.callMetadata, themetadata.tanstackof the current model call. Each assistant message of the call gets a copy of it.With
snapshotStreaming,withPersistencenow storesctx.callMetadataon snapshot rows, so a stopped or failed row has the same metadata as a finished one. A stopped or failed row no longer stores other chunk metadata, such as the durability batch hint ofctx.emitCustomEvent(name, value, { batch: true }). -
#1868
fe83aa0- Remove model ids that providers have retired. Each one now fails on the provider's API, so the types no longer accept it.- Groq:
llama-3.3-70b-versatile,llama-3.1-8b-instant,meta-llama/llama-4-maverick-17b-128e-instruct,meta-llama/llama-4-scout-17b-16e-instruct,meta-llama/llama-guard-4-12b,moonshotai/kimi-k2-instruct-0905,qwen/qwen3-32b. Addsqwen/qwen3.8-27b. - Mistral:
pixtral-large-latest,pixtral-12b-2409,open-mistral-nemo. - OpenAI:
chatgpt-4o-latest,codex-mini-latest,gpt-4o-audio,gpt-4o-mini-audio,gpt-5.6,dall-e-2,dall-e-3,computer-use-preview. The speech modelgpt-4o-audio-previewis replaced bygpt-4o-mini-tts. - Anthropic:
claude-opus-4-1,claude-opus-5-fast. - Gemini:
imagen-4.0-generate-001,imagen-4.0-ultra-generate-001,imagen-4.0-fast-generate-001.
Types and helpers that only served those models are removed:
@tanstack/ai-gemini:GeminiImageProviderOptions,GeminiAnyImageProviderOptions,GeminiAspectRatio,PersonGeneration,SafetyFilterLevel,ImagePromptLanguage,GEMINI_NATIVE_IMAGE_MODELS,isGeminiNativeImageModel. Every Gemini image model usesGeminiNativeImageProviderOptions.@tanstack/openai-base:OpenAIComputerToolName.resolveComputerUseTool(tool)no longer takes a model.@tanstack/ai-openai: the dall-e option and size types.
- Groq:
-
Updated dependencies [
39c1c79,39c1c79,f1b3f65]:- @tanstack/ai-utils@1.0.0
- @tanstack/ai-event-client@1.0.0