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 changeset entries rolled up into release notes. It's the fastest way to scan what changed between two versions.
- 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. Each change names the release it landed in, and the two that need no action say so.
@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 the first release after 0.13.1. 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 the first release after 0.13.1. 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 the first release after 0.13.1. 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 the first release after 0.13.1. 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 the first release after 0.13.1. 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 the first release after 0.13.1. 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 the first release after 0.13.1. 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.
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.