Upgrading
B4.run is pre-1.0 and the API is still moving. So pin a version, read what changed, and upgrade on purpose. This page covers the upgrade workflow and the changes that need action, labeled with the version each one landed in. The full list of changes lives on GitHub.
Fixed-group versioningCopy link to section: Fixed-group versioning
Every publishable B4.run package (@b4run/cli, @b4run/core, @b4run/sdk, the other @b4run/* packages and create-b4-app) is released together under one version number, using Changesets' fixed-group mode. If @b4run/cli is at 0.10.0, so is every other package in that release. You don't need a compatibility matrix. Install matching versions and you're done.
The two Helm charts (b4-sandbox-infra, b4-app) track the same version through their appVersion field, but they're published separately as OCI artifacts. See Kubernetes for how to pull a specific chart version.
How to read what changedCopy link to section: How to read what changed
Each release is built from the changeset entries merged since the previous one. A changeset is a short Markdown file that says what changed and why. You can read them in two places:
- GitHub Releases: github.com/cacheplane/b4run/releases lists every published version with its candidate commit, package artifacts, and verification evidence. Read the per-package changelogs for the changeset entries.
- Per-package
CHANGELOG.md: each package underpackages/*/CHANGELOG.mdhas its own changelog, generated from the same entries but scoped to that package.
Upgrade workflowCopy link to section: Upgrade workflow
- 1
Check the current version
textnpm ls @b4run/clior check the
@b4run/*entries in yourpackage.json. - 2
Read the release notes
Open GitHub Releases and read every entry between your current version and the target version.
- 3
Bump and reinstall
Bump every
@b4run/*dependency inpackage.jsonto the same target version, then reinstall. - 4
PinningCopy link to section: Pinning
Pin @b4run/* packages to a single version (no ^ or ~ ranges) until the project reaches 1.0. A floating range lets an unreviewed bump land in CI or production before anyone reads the notes.
Changes by versionCopy link to section: Changes by version
Newest first. Released changes name their version; unreleased entries describe the next release.
Versioned tool-step emission and restoreCopy link to section: Versioned tool-step emission and restore
Unreleased. Action required if you read raw b4.step CUSTOM events or persisted additional_kwargs.b4_step directly. The runtime now writes B4ToolStep: v: 1, kind: "server", camelCase toolCallId on CUSTOM only, epoch-millisecond timestamps, stable source ids, and an error on failed steps. Read with readToolStep rather than the deprecated readPersistedStep; the new reader accepts both old and new stored values. Types and deprecated exports remain available.
START/END retain any known step metadata without buffering the call. Actual execution-start CUSTOM events follow permission and constraint approval, including after resume. Denied calls never claim they executed. RESULT metadata, replayed /events, and /turns retain failure outcomes, display fields and producer timing. /state remains raw LangChain checkpoint data; it is not an AG-UI message snapshot. Provider tool results are not replayed as client tool results. See Step events.
Refresh application dependenciesCopy link to section: Refresh application dependencies
The 0.15.0 repository production audit has no critical or high findings. Refresh and audit your application lockfile separately; a framework update alone does not replace it. The Hono updates cover CLI test dependencies.
Client response schemas use forwardedProps.responseSchemaCopy link to section: Client response schemas use forwardedProps.responseSchema
Landed in 0.15.0. Action required if your client sends hashbrown.responseSchema.
Move the schema to AG-UI's forwardedProps.responseSchema. The old hashbrown body key is no longer read, so leaving it in place produces an unconstrained reply. B4HttpAgent now accepts responseSchema in its constructor and forwards it on every run. The provider-facing schema name is b4_response.
Allow the key for the route in your server config:
server: {
agui: {
clientForwardedProps: { "/chat": ["responseSchema"] },
},
}The object form accepts true or an array of allowed keys for each route (or "*" for all routes). The existing array form still permits any key on its listed routes. An unaccepted key returns 422 forwarded_props_not_allowed; malformed settings fail at startup. See AG-UI.
Development-time CLI usage reportingCopy link to section: Development-time CLI usage reporting
Landed in 0.15.0. No API migration is required. The CLI displays a one-time notice before reporting anonymous usage signals for dev, build, add, check, typegen, verify, and eval.
Set B4_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1 in your shell before running the CLI to disable reporting, the notice, and the usage-id file. A project .env also disables sends, but loads after the first-run notice and id-file setup. CI is silent automatically; b4 start, runtime packages, and create-b4-app never report. See Privacy.
b4.step is a versioned contractCopy link to section: b4.step is a versioned contract
Landed in 0.15.0. Action required only if you switch exhaustively over StepStatus from @b4run/ag-ui/view. Everything you already send and read keeps working.
A tool call's step now has a versioned contract, B4ToolStep from @b4run/sdk (v: 1, camelCase), with readToolStep, mergeToolStep, JSON Schemas at @b4run/sdk/tool-step.schema.json and @b4run/sdk/answer.schema.json, and a fixture corpus in @b4run/sdk/testing. PersistedStep, readPersistedStep, B4StepEventValue, B4StepStatus, B4_STEP_STATUSES and readStepEvent stay exported and are deprecated; the old shapes still read. That release retained legacy emitters; the unreleased emission/restore update above moves the runtime to v1. These behaviors changed:
StepStatusgainscancelled. Aswitchover it that TypeScript checks for exhaustiveness needs acancelledcase.- A cancelled run settles its open steps as
cancelledinstead offailed, so a row that showed "failed" after a cancel now shows as cancelled. An interrupt cancels the open calls it doesn't park; the calls it names, and anypendingToolCallIds, stay open. - A successful run no longer fails the calls it left open. A restored thread whose turn ended without a call's result still shows that call as failed.
- A
TOOL_CALL_RESULTwith no step of its own completes a running step and clears its running label. A result with a top-levelerrorfails the step, even after acompletedstep. readToolStepreturnsundefinedfor avother than 1, andreadStepEventdrops an empty-stringlabel.
See Step events.
@b4run/ag-ui has one folder per framework; the stylesheet is @b4run/ag-ui/styles.cssCopy link to section: @b4run/ag-ui has one folder per framework; the stylesheet is @b4run/ag-ui/styles.css
Landed in 0.14.0. Action required if you import the activity stylesheet, or the CopilotKit connector from a pre-release build.
The activity stylesheet moved from @b4run/ag-ui/react/styles.css to @b4run/ag-ui/styles.css, one sheet for every kit; ./react/styles.css is gone. Change the import:
import "@b4run/ag-ui/styles.css"The React CopilotKit connector (B4Activity, useB4ChatSlots, useB4Turns, useB4ActivityContext) is at @b4run/ag-ui/react/copilotkit, not ./copilotkit. The Angular activity kit ships in the same package: @b4run/ag-ui/angular, with its connectors at ./angular/events and ./angular/copilotkit; Angular and @copilotkit/angular are optional peer dependencies. See AG-UI and Web Clients.
The legacy activity cards are gone; @b4run/ag-ui/react is the activity kitCopy link to section: The legacy activity cards are gone; @b4run/ag-ui/react is the activity kit
Landed in 0.14.0. Action required if you render B4.run activity with the 0.13 cards, renderers or subagent hook.
Removed from @b4run/ag-ui/react: PlanActivityCard, ActivityChecklist, SubagentPanel, the classNames/components props with B4ActivityClassNames, B4ActivityComponents, B4TodoRowProps and B4ToolRowProps, cx, planActivityContentSchema, and useSubagentRuns with SubagentEventSource. Removed from @b4run/ag-ui/view (and their ./react re-exports): reduceSubagentRuns, EMPTY_SUBAGENT_RUNS, SubagentRun, SubagentRunsState and SubagentToolCall. Removed from @b4run/ag-ui/react/copilotkit: b4ActivityRenderers and b4PlanActivityRenderer, so drop renderActivityMessages={b4ActivityRenderers} from <CopilotKit>. isSubagentMessage now comes from @b4run/ag-ui/view only.
What replaces them: wrap <CopilotChat> (or <CopilotSidebar>) in B4Activity from @b4run/ag-ui/react/copilotkit and spread useB4ChatSlots() onto it. Each turn renders as one TurnActivity: its tool steps, its plan (the b4.plan snapshots), its reasoning, and each subagent as a step with the child's own steps nested. A parked interrupt renders as an ApprovalCard, so remove your own useInterrupt for B4.run permission prompts (CopilotKit has one interrupt slot, and B4Activity fills it). A host with its own transcript uses useB4Turns(agent) with TurnActivity and ApprovalCard; a non-React client folds events with reduceTurns from @b4run/ag-ui/view. To restore a thread after a reload, give the CopilotKit runtime createB4AgentRunner(InMemoryAgentRunner, { url, fetch }) from @b4run/ag-ui/copilotkit-runtime.
The stylesheet now sits in @layer b4-activity and styles only the kit. The .b4-activity* rules and the --b4-activity-gap, -font-size, -margin, -padding, -header-weight and -badge-bg tokens are gone; the design tokens --b4-activity-surface-alt, -running-bg, -failed-bg, -primary, -on-primary, -radius-card, -radius-pill and -font-mono are new, and -running (#2563eb → #b45309), -failed (#9f1239 → #b91c1c), -complete (#15803d), -radius (10px → 8px), -surface, -border, -text and -muted are re-valued. Dark mode no longer follows the OS by default; it follows the host (.dark, [data-theme="dark"], or data-b4-theme="dark" on the root). Action required if your app relied on OS-driven dark: set data-b4-theme="auto" on the root element, or override the tokens in your own CSS. See AG-UI › Consuming it from a web UI.
Threads restore their activity from GET /threads/:thread_id/turnsCopy link to section: Threads restore their activity from GET /threads/:thread_id/turns
Landed in 0.14.0. Action required if you read GET /threads/:thread_id/state to rebuild a transcript, or parse values.messages yourself.
Every tool call's ToolMessage now carries a complete additional_kwargs.b4_step (status, start and settle times, the permission decision, and the display's icon, label and sources), the task tool returns a ToolMessage with b4_subagent, and the runtime stamps b4:turn on the head checkpoint when a run ends. A call that a permission or argument constraint denied persists and restores as a denied step, not a failed one. GET /threads/:thread_id/turns rebuilds the same TurnsView a live AG-UI client builds, approvals included, so prefer it over parsing /state. /state.created_at is now the checkpoint's time. Threads written before this release do not restore through /turns. See Agent Protocol.
AG-UI TOOL_CALL_RESULT.content is the tool's outputCopy link to section: AG-UI TOOL_CALL_RESULT.content is the tool's output
Landed in 0.14.0. Action required only if an AG-UI client parsed TOOL_CALL_RESULT.content as a serialized LangChain ToolMessage ({ lc, type, id, kwargs: { content } }).
content is now the text the model received: the tool's return value, or the error text for a tool that threw. Drop any kwargs.content unwrapping, and parse JSON only if your tool returns JSON. Permission interrupts also gain toolCallId, subagentRunId for a child's tool, command or memory gate, and responseSchema (once/always/deny); these are additive. See AG-UI.
Subagents are AG-UI SUBAGENT_* events, not a b4.subagent activityCopy link to section: Subagents are AG-UI SUBAGENT_* events, not a b4.subagent activity
Landed in 0.14.0. Action required for AG-UI clients that rendered the b4.subagent activity.
The b4.subagent activity is gone, with B4_SUBAGENT_ACTIVITY_TYPE, B4SubagentActivityContent, SubagentActivityCard, b4SubagentActivityRenderer and subagentActivityContentSchema. A subagent is now SUBAGENT_STARTED (its subagentRunId is the task tool-call id), everything it does arrives as the ordinary AG-UI events tagged subagentRunId, and it closes with SUBAGENT_FINISHED or SUBAGENT_ERROR before the run ends. The task tool call is an ordinary tool call again, with its own frames and result. In React, B4Activity shows each subagent as a step in its parent's turn, with the child's own steps nested (see the activity kit entry). GET /agui/{routeId} advertises multiAgent. See AG-UI › Subagents.
Subagent stream events carry the root shapesCopy link to section: Subagent stream events carry the root shapes
Landed in 0.14.0. Action required only if you read subagent.* events off /runs/stream or the attach digest.
subagent.message { call_id, chunk } is now subagent.token { call_id, …identity, data, messageId }, and subagent.tool_call / subagent.tool_result carry name (not tool) under the model's tool-call id (not the execution run id). New: subagent.reasoning, subagent.message_end, subagent.tool_call_args, and parent_call_id / description on subagent.start. @b4run/testing's RunResult.subagents reads the new shapes. See Streaming output.
reasoning is keyed by providerCopy link to section: reasoning is keyed by provider
Landed in 0.14.0. Action required if an agent() route sets reasoning.
agent({ reasoning: { effort } }) is now agent({ reasoning: { openai: { effort } } }), and Anthropic routes gain reasoning: { anthropic: { budgetTokens } }. The flat effort is an unknown key and fails the route when its model is built, as does a block for a provider the route does not resolve to. Before, a misplaced setting was silently ignored (and the OpenAI effort itself never reached the request). Setting openai.summary moves the route to the OpenAI Responses API and streams the summary as AG-UI REASONING_* events; GET /agui/{routeId} advertises reasoning.supported accordingly. See Reasoning.
src/auth.ts resolves the caller; ThreadAccessRequest.headers is goneCopy link to section: src/auth.ts resolves the caller; ThreadAccessRequest.headers is gone
Landed in 0.14.0. Action required if a thread-access policy reads identity from request headers.
ThreadAccessRequest.headers is removed. Move the header read into src/auth.ts, which default-exports defineAuth({ authenticate }), and read the caller as req.principal in the policy. The same principal reaches middleware as req.principal and tools as ctx.principal. An auth file that does not default-export defineAuth fails the boot with B4_E3005. If the policy is "each caller reaches only its own threads", ownedThreads from @b4run/sdk replaces it. See Access control and Thread access.
An unresolved memory scope makes memory unavailableCopy link to section: An unresolved memory scope makes memory unavailable
Landed in 0.14.0. Action required if a route's memory.ts declares user or tenant and memory.resolveScope doesn't always set it.
A dimension the route declares and resolveScope leaves without a value now makes memory unavailable for that request: remember and recall answer that memory is unavailable, and nothing is read or written. Before, the request fell back to the shared workspace+route namespace. Resolve the dimension (resolveScope now receives principal), or drop it from memory.ts. With a src/auth.ts, GET /memory/candidates lists only the caller's namespaces; canReviewMemory(principal) on defineAuth lets a reviewer see all of them. See Long-term memory.
POST /agui answers protobuf to */*; encodeAgUiSse is replacedCopy link to section: POST /agui answers protobuf to */*; encodeAgUiSse is replaced
Landed in 0.14.0. Action required if a client calls POST /agui/:routeId without naming text/event-stream, or your code imports encodeAgUiSse.
POST /agui/:routeId answers with AG-UI's protobuf binding whenever Accept admits application/vnd.ag-ui.event+proto, including through */*. curl, and fetch without an accept header, both send */*: send accept: text/event-stream to keep SSE. @ag-ui/client and CopilotKit already name SSE and are unaffected. @b4run/ag-ui/sse no longer exports encodeAgUiSse(event, accept); use encodeAgUiEvent(event, accept) for the bytes and agUiContentType(accept) for the content-type header, or encodeAgUiEvent(event) without accept to always write SSE. See AG-UI › Transport.
Message content can be a list of content partsCopy link to section: Message content can be a list of content parts
Landed in 0.14.0. Action required if your code narrows message content to string.
B4Message.content (@b4run/ag-ui), UnwrappedToolResult.content (@b4run/langchain) and MiddlewareAfterMessage.content (@b4run/sdk) are now string | readonly B4ContentPart[]. Chain, graph and workflow routes receive messages[].content as that part array whenever the client sent parts, where it used to be flattened to text. Handle the array case. See AG-UI › Multimodal content.
Custom grant and tool-call stores need new methods and fieldsCopy link to section: Custom grant and tool-call stores need new methods and fields
Landed in 0.14.0. Action required only if you wrote your own InterruptGrantStore or ClientToolCallStore. The SDK memory stores, @b4run/sqlite-storage and @b4run/postgres-storage already implement everything below, with their own migrations for the new columns.
InterruptGrantStoreneedsprune({ before }), and must store and returnconsumedBy(consume()takes an optionalby). While grants are on, a store missing a method fails the boot naming it.ClientToolCallStoreneedsprune({ before })andsettle, and its records gainkind("client" | "server"),settledAtand a requiredparentToolCallId(nullat the root).resultwidens fromstringtoB4MessageContent, so a custom store must keep and return content parts. A store set inserver.agui.clientToolStorethat is missing a method fails the boot naming it.- A custom
SubagentResolverin@b4run/langchainmust returnResolvedSubagentGraph.routeKey(<routeId>#<mode>).
See Approval grants and AG-UI › The tool-call record.
CopilotKit 1.76.0 or laterCopy link to section: CopilotKit 1.76.0 or later
Landed in 0.14.0. Action required if your app uses @copilotkit/react-core below 1.76.0.
@b4run/ag-ui's optional @copilotkit/react-core peer range is >=1.76.0, the first release whose AG-UI client speaks 1.0. npm 7+ rejects an older one with ERESOLVE; pnpm warns. Upgrade CopilotKit together with @b4run/ag-ui. @copilotkit/runtime is no longer a peer of @b4run/ag-ui: the runtime route passes its own InMemoryAgentRunner class to createB4AgentRunner, and the B4AgentRunner class export is gone.
The research template is now navlogCopy link to section: The research template is now navlog
Landed in 0.14.0. Action required if you scaffold with --template research.
npm create b4-app -- --template navlog scaffolds the flight planner that replaces the research template. --template research still works in 0.14 and prints a deprecation notice; @b4run/devkit's resolveTemplateDir accepts only basic and navlog. Apps already generated from the research template are unaffected.
B4.run speaks AG-UI 1.0Copy link to section: B4.run speaks AG-UI 1.0
Landed in 0.13.1. Action required for AG-UI clients.
B4.run serves AG-UI 1.0 (@ag-ui/core 1.0.x; @ag-ui/client >=1.0.1 <2.0.0), so upgrade your client: CopilotKit 1.76.0 is the first release that speaks it. Against 0.13.0, a cancelled or shut-down run now ends with RUN_FINISHED { outcome: cancelled } instead of RUN_ERROR, a request declaring another protocol major is refused with 400 unsupported_protocol_version, and a null route result is omitted instead of sent as result: null. RUN_STARTED declares protocolVersion: "1.0". 0.14.0 adds media parts; see Multimodal content. See AG-UI.
Client tools and forwardedProps are refused unless the route opts inCopy link to section: Client tools and forwardedProps are refused unless the route opts in
Landed in 0.13.1. Action required if an AG-UI client sends tools or forwarded props, such as CopilotKit's useFrontendTool or its generated suggestions.
POST /agui/:routeId now refuses a non-empty tools or forwardedProps with 422 (client_tools_not_allowed / forwarded_props_not_allowed, under B4_E5401) unless the route is named in server.agui. Empty tools: [] and forwardedProps: {} are unaffected, and static suggestion lists and render-only hooks send neither. To accept them, opt the route in:
server: { agui: { clientTools: ["/chat"], clientForwardedProps: ["/chat"] } }On an opted-in route the model can call the client's tools; they need a tool-call store on edge, serverless and multi-instance deployments. The same release validates the envelope (threadId, runId, state) with 422 invalid_envelope and friends, caps request bodies at 8 MiB (413 payload_too_large), and reserves tool names starting with client_: a route with an authored or capability tool named client_* fails preparation. See AG-UI › Envelope validation and Client-provided tools.
retry applies to each model call, not the whole runCopy link to section: retry applies to each model call, not the whole run
Landed in 0.13.1. Action required if an agent() route sets retry, or relied on a failed run being restarted.
maxAttempts (default 3) now becomes the chat model's maxRetries (maxAttempts - 1), so each model request is retried, and maxAttempts: 1 fails fast. Before, LangChain's own 6 retries applied whatever retry said, and B4.run restarted the whole run on top of that. The run is never restarted now, so tools never run twice, and a transient error outside the model call fails the turn. An invalid retry (a bad maxAttempts or baseDelay, an unknown key, a non-object) fails the route when it first runs. b4 memory consolidate / reflect take attempts from the new memory.distill.retry: { maxAttempts }, and a malformed memory.distill fails with B4_E1009. See Retry.
Rebuild node apps that have a thread-access policyCopy link to section: Rebuild node apps that have a thread-access policy
Landed in 0.13.0. Action required: this is a security fix.
A built node app looked for src/thread-access.ts on disk at boot, and a missing file read as "no policy", so every thread endpoint was served ungated. The policy is now part of the build, and a missing policy or a manifest older than it stops the server from starting. Rebuild and redeploy. The built manifest imports the policy as it loads, including in b4 check after b4 build, so a policy that reads a required environment variable at import time needs it there too. The hono and vercel targets already worked this way. See Thread access › Build targets.
Unknown sandbox keys are refusedCopy link to section: Unknown sandbox keys are refused
Landed in 0.13.0. Action required if your sandbox block has a key other than workspace, thread, provider, network, env, resources, security or idleTimeoutMs.
b4 check, b4 build and startup now refuse any other key, and thread beside workspace. A misspelt key used to be ignored silently, which left every thread in a per-app sandbox. Rename or remove it. The same release changes the default network to plain { mode: "allow" }; the old default's metadata-endpoint denylist was never enforced, so runtime behaviour is unchanged. On a cloud VM, set network: { mode: "deny" } or block that endpoint outside B4.run. See Sandbox › Network policy.
ThreadAccessRequest.requestedWorkspace and new thread operationsCopy link to section: ThreadAccessRequest.requestedWorkspace and new thread operations
Landed in 0.13.0. Action required if you build a ThreadAccessRequest by hand or switch exhaustively over ThreadOperation.
ThreadAccessRequest gains the required field requestedWorkspace (undefined everywhere except the staged-workspace operations), and ThreadOperation gains "workspace.source.put" and "thread.workspace". Add requestedWorkspace: undefined to hand-built requests and cases to exhaustive switches. POST /threads now refuses a workspace field it won't serve with 400 workspace_not_accepted instead of ignoring it. See Thread access › Workspace endpoints.
Denying writeFile also withholds editFileCopy link to section: Denying writeFile also withholds editFile
Landed in 0.13.0. No action required for read-only routes.
Workspace agents gain an editFile tool and ranged readFile. A route that denies writeFile loses editFile too, so deny: ["writeFile"] routes stay read-only; name editFile in allow to keep it. An allow-list that names writeFile doesn't grant editFile. See Access control › Tool scoping.
b4 eval --record refuses a recording replay would rejectCopy link to section: b4 eval --record refuses a recording replay would reject
Landed in 0.13.0. Action required only if a recording run now exits 2.
getRecordedFixtures() validates each turn as replay does and throws naming the rejected turn, and b4 eval --record reports Refused to record <eval> › <case>, exits 2 and doesn't write that case's fixture. The usual cause is an empty closing assistant message; if the run's answer is a tool call, export returnDirect from that tool. Re-recording an existing tape also adds a usage field to each response. See Evals › Recording fixtures.
Agent routes run on LangChain createAgentCopy link to section: Agent routes run on LangChain createAgent
Landed in 0.12.0. Action required if you pin @langchain/core below 1.2.12, or a returnDirect tool relied on its errors ending the run.
Agent routes moved from LangGraph's deprecated createReactAgent to LangChain's createAgent. A returnDirect tool now ends the run only when it succeeds: a thrown tool or arguments that fail its schema go back to the model, which can retry. @b4run/langchain depends on langchain ^1.5.12, and its @langchain/core peer range rises to ^1.2.12. A route without summarization or a returnDirect tool keeps the same number of graph steps, so recursionLimit budgets are unchanged. See Tools › Ending the run from a tool.
LangChain packages move to their current releasesCopy link to section: LangChain packages move to their current releases
Landed in 0.11.0. Action required if your app pins LangChain packages directly.
The peer ranges rise to @langchain/core 1.2.12, @langchain/langgraph 1.4.17, @langchain/langgraph-checkpoint 1.1.5, @langchain/openai 1.5.13, @langchain/anthropic 1.5.11, @langchain/google-genai 2.3.2, @langchain/xai 1.4.13 and @langchain/openrouter 0.4.13. Upgrade your direct pins to match, and keep a single copy of @langchain/core and @langchain/langgraph in your lockfile. 0.11.1 was never published; 0.11.2 carries its fixes.
basic is the default templateCopy link to section: basic is the default template
Landed in 0.11.0. Action required only if your scripts scaffold without --template and expect the research workspace.
npm create b4-app@latest my-agent now scaffolds basic: one /hello agent route with a typed greet tool. Pass --template to choose another; 0.14.0 renamed the research template to navlog.
Tool-call arguments stream in several deltasCopy link to section: Tool-call arguments stream in several deltas
Landed in 0.10.0. Action required only if an AG-UI client assumed one TOOL_CALL_ARGS per call.
When the provider streams argument fragments, a tool call is now one TOOL_CALL_START, several TOOL_CALL_ARGS deltas and one TOOL_CALL_END. Their concatenation equals the single delta sent before. Tool execution still receives the complete, parsed arguments. See AG-UI › Outbound events.
Tool schemas use your tsconfig.json and fail on unresolved input typesCopy link to section: Tool schemas use your tsconfig.json and fail on unresolved input types
Landed in 0.9.0. Action required if b4 typegen or b4 verify now fails with UnresolvedToolInputTypeError.
Tool schema extraction reads the nearest tsconfig.json above the app root, honouring extends, paths and baseUrl, so an input type imported through a path alias derives its real schema. A declared input type that still resolves to any or unknown, or an import that doesn't resolve, now fails with UnresolvedToolInputTypeError naming the tool, the type and the tsconfig. Before, the tool silently got { properties: {} }. Fix the import or the type. Tools that take no input keep working.
hashbrown.responseSchema is applied or refusedCopy link to section: hashbrown.responseSchema is applied or refused
Landed in 0.9.0. Action required only if a client sends hashbrown.responseSchema to a route or provider that can't honour it.
POST /agui/:routeId used to accept the field and ignore it. On an agent route the schema now constrains the final message (OpenAI response_format, Anthropic output_config.format). A malformed schema, a non-agent route or a provider with no schema mode gets 422 under B4_E5402 before the run starts. See AG-UI › Response schema.
A tool that throws emits TOOL_CALL_RESULTCopy link to section: A tool that throws emits TOOL_CALL_RESULT
Landed in 0.9.0. No action required unless an AG-UI client relied on a failed call having no result.
A thrown tool used to produce TOOL_CALL_START, TOOL_CALL_ARGS and TOOL_CALL_END with no TOOL_CALL_RESULT. It now gets a result keyed by the same tool-call id, carrying the error the model received. @b4run/testing's run.toolResults marks it isError.
The app root must declare "type": "module"Copy link to section: The app root must declare "type": "module"
Landed in 0.8.35.
b4 check, dev, build, verify, routes, and typegen now fail with
B4_E1006 when the app root's package.json lacks "type": "module",
where they used to run. Add the field:
{
"type": "module"
}Apps scaffolded by create-b4-app already have it. An app that never set it
was in one of two states, and both are fixed by the same line:
- Routes exported with
export default agent(...)were silently dropped.b4 checkreported0 routes discoveredand exited 0, because the route modules were compiled as CommonJS and their default export arrived wrapped in the ESM/CommonJS interop object. - Routes exported as
export const workflow(orgraph/chain) appeared to work. Node's CommonJS lexer hoists those into named exports, so discovery found them. Butb4.config.tswas loaded through the same interop wrapper, so every setting on it was silently ignored and the defaults applied.
A route index.ts that exports nothing B4.run recognises is now also an error
(B4_E1007) naming the file. Before, discovery silently skipped the directory.
If such a directory isn't a route, prefix its name with _ to keep it out of
route discovery.
The vercel target's function is now b4.funcCopy link to section: The vercel target's function is now b4.func
Landed in 0.8.35.
b4 build with the vercel target publishes its runtime function at
functions/b4.func/ and routes /(.*) to /b4. It was functions/index.func/
routed to /index, except in a build that configured build.vercel.static,
which already used b4.
Why? In the Build Output API, a function named index is also served at /,
where it shadows a static index.html. That hazard comes with the name in every
build, so we moved the name off the root. An app that adds a frontend later
doesn't have to rename its function.
The function itself is the same, and anything that goes through the CLI keeps working. What breaks is anything that reads the published tree by path:
- A deploy script that copies or inspects
functions/index.func. - An external assembler that composes the tree after the build.
- CI that imports the built entry for a smoke check.
- A hand-written
config.json, or an assertion, naming the/indexroute.
Migrating depends on whether you ship static assets. Without static assets, you can pin the old name:
// No static assets: you can keep the old published path indefinitely.
export default config({
build: { targets: ["vercel"], vercel: { functionName: "index" } },
})With static assets, the same setting fails the build:
// With build.vercel.static: this FAILS the build, by design. A function named
// "index" would shadow the static root, so the build refuses it and names the
// conflict. Update whatever reads the tree instead.
export default config({
build: {
targets: ["vercel"],
vercel: { functionName: "index", static: { dir: "../dist/react" } },
},
})An app that ships a frontend with the runtime must move to the new path. An
explicit index shadows the static root just as the old default did, so
allowing it would bring back the original bug where it does the most damage.
Either way, functionName accepts one path segment of letters,
digits, _ or -, and b4 check rejects anything else before the build writes.
Docker and Kubernetes sandboxes now require a scopeCopy link to section: Docker and Kubernetes sandboxes now require a scope
Landed in 0.8.32. Action required if you use the Docker or Kubernetes sandbox provider.
Both providers now take a required scope: a stable application and environment identifier such as my-app-production. Every container, volume, Pod, PVC and NetworkPolicy name is a hash of the scope and the thread ID, which prevents collisions the old thread-name normalization allowed:
provider: dockerSandbox({ scope: "my-app-production", image: "node:24-slim" }),Resource names changed, so existing sandboxes aren't reattached, migrated or deleted. Export any data you need before you upgrade, then clean up the old resources yourself. Scope isn't authorization: your application still decides who can use a thread. See Execution Sandbox.
Skills, plan.md, and route memory.md now work on edge targetsCopy link to section: Skills, plan.md, and route memory.md now work on edge targets
Landed in 0.8.30. No action required.
The hono and vercel targets used to fail the build when a route shipped
skills, and plan.md and memory.md silently did nothing there. b4 build
now bundles all three into modules.edge.mjs and serves them from the
manifest at request time. An app that removed its skills to pass the gate can
restore them. Files over the build's per-marker cap fail the build with
B4_E1005 by name (b4 check applies the same limits). The cap is 32 KiB for
SKILL.md (a build-only cap) and memory.md, and 64 KiB for plan.md. The
last two match the runtime limits. See Skills, plan.md, and route memory.md are
bundled.
toolOutput is now gated off the hono targetCopy link to section: toolOutput is now gated off the hono target
Landed in 0.8.22, first published in 0.8.24.
If your app configures toolOutput and names "hono" in build.targets,
b4 build and b4 check now fail with B4_E1005 where they used to pass.
Your app didn't change. The gate did. Tool-output
offloading spills oversized tool results to a file
under workspace/ and hands the model a pointer to it. An edge runtime has no
filesystem to spill to, so the feature never worked on that target. It slipped
through because it was the only gated feature whose config is plain JSON. The
other gated keys are live objects that the build strips. toolOutput was
inlined into the bundle and then ignored at runtime. A green build with a worker
that silently never offloads is worse than a failed build.
You have two options. Remove toolOutput from b4.config.ts, or drop "hono"
from build.targets and deploy with the node target, which serves offloading
normally. An empty toolOutput: {} expresses no intent, so the gate lets it through.
Node deployments are unaffected. The gate is specific to the hono build
target, and the matching runtime check (below) can't fire on Node at all.
Gated features now fail loudly at request time, not just at build timeCopy link to section: Gated features now fail loudly at request time, not just at build time
Landed in 0.8.22, first published in 0.8.24.
sandbox and toolOutput used to be read and then
quietly do nothing on a runtime with no filesystem. Both now raise
B4_E1005 on every request, naming the feature and the config key that
introduced it. A manifest that records skills but bundles no bodies for them is
reported the same way. See What the edge cannot
serve.
This closes a gap the build gate couldn't. Composing an entry by hand over
@b4run/cli/fetch is a supported way to deploy, and such an app never runs the
hono target, so it never meets the build gate.
Node apps are safe here, whatever they configure. The check short-circuits before it reads a single config key whenever the runtime supplied filesystem fallbacks, and every Node entry point always supplies them. An absent sandbox on Node still degrades as documented. On the edge, the affected config was already doing nothing.
@b4run/postgres-storage now requires a pool on its main entryCopy link to section: @b4run/postgres-storage now requires a pool on its main entry
Landed in 0.8.21.
connectionString has moved to a new @b4run/postgres-storage/node subpath.
Through 0.8.19 the main entry accepted either and built its own pg pool from a
connection string. Now it imports pg for types only, so the package links
on a runtime with no TCP sockets. That's what makes the
hono edge target possible.
connectionString is gone from the main entry's option type, so passing it
there is a type error, and the factory throws at construction naming the missing
pool. You have two ways to migrate:
// 1. Change the import: same factories, connectionString still works, and the
// store still builds and owns its pool.
import { postgresCheckpointer } from "@b4run/postgres-storage/node"
// 2. Or build the pool yourself and keep the main entry. This is what you want
// anyway when one pool serves all three stores.
import { Pool } from "pg"
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
// Do not skip this. See below.
pool.on("error", (error) => {
console.error("postgres pool client error (connection dropped):", error)
})
postgresCheckpointer({ pool })The /node entry re-exports everything the main entry does, so option 1 is
usually one line. Pool ownership works the same in both shapes. close() ends a
pool the store built, and an injected pool is left alone (ownsPool, defaulting
to false).
This shipped as a patch even though it breaks, which is one more reason to pin a version and read the notes.
Node-only imports moved to /nodeCopy link to section: Node-only imports moved to /node
Landed in 0.8.17.
If your tooling imports filesystem or process-backed helpers, change the import specifier. The symbols stay the same:
- From
@b4run/core/node:discoverRoutes,findB4App,assertB4RoutesDir,extractToolSchemasForRoute,extractToolTypesForRoute, andregisterTsxLoader. - From
@b4run/permissions/node:createPermissionsStore. - From
@b4run/workspace/node:localFilesystemandlocalExec.
For example, change import { discoverRoutes } from "@b4run/core" to import { discoverRoutes } from "@b4run/core/node". Runtime-agnostic contracts and helpers stay on each package's main entry.
kind: "reflection" is now acceptedCopy link to section: kind: "reflection" is now accepted
Landed in 0.8.17.
Memory distillation wired the reflection kind.
defineMemory({ kind: "reflection" }) and the generated remember tool now
accept it. Before, it was typed but threw "memory kind 'reflection' is not
yet wired" at write time. Reflections are append-only (like episodic
writes): a later insight never supersedes an earlier one, and ask mode never
prompts for one. b4 memory reflect produces them, as candidate records by
default.
procedural remains the one typed-but-unwired kind and still throws.
No action required. This change is additive.
Node 24 is the minimumCopy link to section: Node 24 is the minimum
Landed in 0.8.16.
B4.run packages declare node >=24.0.0, and create-b4-app refuses to scaffold on older releases. Before upgrading dependencies, move local version-manager files, CI runners and custom container bases to Node 24 or later. Then reinstall with that runtime and run b4 verify. Node 24 also supplies the npm and unflagged node:sqlite versions the scaffold expects.
MemoryStore now requires pruneCopy link to section: MemoryStore now requires prune
Landed in 0.8.15.
Episodic memory added a required retention
method to the MemoryStore contract. If you implement a custom store for
config.memory.store, add it:
prune(opts: {
now: string // the clock: rows with expiresAt <= now are expired
namespacePrefix?: string // scope the pass to matching namespaces
cap?: number // per-namespace cap for episodic records
}): Promise<{ deletedExpired: number; deletedOverCap: number }>Semantics:
- TTL: delete every record whose
expiresAtis at or beforenow(records withoutexpiresAtnever expire). - Cap: within each namespace, keep at most
capepisodic records, deleting the oldest beyond it. Nocapmeans no cap pass.
The runtime episode recorder calls prune lazily after each write, and
b4 memory prune runs it manually.
Two related behavior changes in the same release:
searchandbrowseacceptsince/until(ISO instants, withsinceinclusive anduntilexclusive), comparing againsteffectiveAtwith acreatedAtfallback.- When a query supplies
now,searchandbrowsemust exclude expired rows (expiresAt <= now). Queries withoutnoware unchanged.
The built-in stores implement all of this, and runMemoryStoreConformance
checks a custom one.
MemoryStore now requires browse and statsCopy link to section: MemoryStore now requires browse and stats
Landed in 0.8.14.
The MemoryStore contract gained two required methods to power the
Inspector's Memory panel. If you implement a custom store for
config.memory.store, add both:
browse(q?): cross-namespace/status listing, returning{ records, total }withrecordsorderedupdated_at DESC, id ASCandtotalcounting all matches (ignoringlimit/offset). The optional query narrows bynamespacePrefix,status,kind, andsourceType.stats(opts?): aggregate counts:{ total, byStatus, byKind, byNamespace, bySourceType }, optionally scoped to anamespacePrefix.
The built-in SQLite and pgvector stores already implement both, and the
runMemoryStoreConformance kit in @b4run/testing covers them. Run it
against a custom store to verify the contract.
Two related behavior changes in the same release:
- The config-facing store type is now the full
MemoryStorecontract (deleteandlistCandidatesincluded), where it used to be the narrower capability-facing surface. A custom store that already satisfied the CLI'sb4 memorycommands is unaffected. b4 memory approve(and the Inspector's Approve) now reconciles supersession: approving a candidate that contradicts an active record with the same identity key supersedes the old record, so you no longer get two active rows. Approving an identical duplicate dedupes it.