Release · v0.8.21
B4.run 0.8: The framework around the agent
Learn what 0.5 through 0.8 added around the agent: evals, tool permissions, sandboxes, delegation, typed memory, AG-UI clients and production builds.
An agent starts with a model and a few tools. Then the application around it raises more questions. How do we test its behavior? Which actions require approval? What should it remember? How does a browser display its progress?
B4.run's route conventions give those pieces a place to live. The releases from 0.5 through 0.8.21 added evaluations, tool permissions, long-term memory and production runtime options.
Let's walk through them with a support assistant that looks up an order, researches a policy and prepares a reply.
Version note: This post rounds up everything through 0.8.21. The examples use the current @b4run package names, which arrived in 0.8.27, so follow the linked upgrade guide for a current installation. Those names don't exist at 0.8.21.
GoalsCopy link to section: Goals
We want our support application to:
- Check its behavior against repeatable scenarios.
- Limit tool access and request approval where needed.
- Delegate a specific task to a specialist agent.
- Retain useful information across conversations.
- Connect to a browser and run outside the development server.
You can add each of these when you need it. A small agent doesn't need them all on day one.
Evaluate agent behaviorCopy link to section: Evaluate agent behavior
A unit test can tell us that lookupOrder() returns the expected order. It can't tell us whether a model will choose that tool when a customer asks about a delivery.
An evaluation, or eval, runs a set of scenarios and scores their results. The scenarios form a dataset. A scorer checks one aspect of the result, such as required text, a tool call or a budget. A gate decides whether the collected scores meet our chosen threshold.
Start with a small evalCopy link to section: Start with a small eval
An eval lives beside its route under evals/*.eval.ts. Here is a small example for a /support route:
// src/app/support/evals/greeting.eval.ts
import { contains, defineEval } from "@b4run/evals"
import { script } from "@b4run/testing"
export default defineEval({
name: "support greeting",
dataset: [
{
name: "offers help",
input: "hello",
fixtures: script().user("hello").replies("Hi! How can I help?"),
},
],
scorers: [contains("help", { threshold: 1 })],
threshold: 1,
})A few things to note:
- The file's location associates it with the support route.
datasetcontains one scenario with a user input and a scripted model response.contains()checks whether the final text containshelp.- The outer
thresholdsets the mean score required for the eval to pass.
A greeting alone isn't much of a quality bar for a support application. Add scenarios for the work the agent should do, including missing orders, ambiguous requests and service failures.
Replay and live runsCopy link to section: Replay and live runs
By default, b4 eval replays model fixtures. Our tools, prompts, capabilities and state run, while the model response comes from the fixture. This makes the scenario repeatable in CI.
Of course, a passing replay can't show that a live model will make the same decision, because we supplied its response. Use b4 eval --live to measure real model behavior, and --record to capture responses for later replay. Both make provider calls and cost money.
Scorers run after the agent finishes. If a scorer uses another model to judge the answer, that model call also needs a fixture or mock for an offline CI run.
For smaller checks, @b4run/testing includes harnesses for agents, tools, middleware, workspaces and protocol behavior. See Evals and Testing agents for complete examples.
Control tool executionCopy link to section: Control tool execution
Our support agent might look up orders on its own, while a refund needs a person's approval. There are three separate decisions here:
| Question | Framework mechanism |
|---|---|
| Should the model have access to this tool? | Tool scoping with allow and deny rules |
| Should this particular call execute? | Argument constraints and approval rules |
| Where will the approved operation run? | The host environment or a configured sandbox |
Let's look at the first two using a route configuration:
import { agent } from "@b4run/sdk"
export default agent({
model: "gpt-5-mini",
systemPrompt: "Help customers with their orders and refund requests.",
tools: {
deny: ["deleteAccount"],
approve: ["issueRefund"],
},
})The deny rule removes deleteAccount from the tools available to this agent. The approve rule pauses an issueRefund call for a decision before it runs. Both names refer to tools the application implements. The rules only govern them.
Argument constraints can inspect the proposed input too. For example, a refund above a certain amount could need extra review. The constraint can allow the call, reject it with a reason or request approval.
The tool still needs to verify the authenticated customer's access to the order. Model instructions and route parameters can't stand in for authorization in application code.
Isolate filesystem and shell workCopy link to section: Isolate filesystem and shell work
A workspace gives file operations a configured root directory. Permission checks govern shell commands and access outside that workspace. Still, an approved operation that runs on the application host runs with the host's access.
A sandbox provides a separate execution environment for filesystem and shell work. B4.run has Docker and Kubernetes providers.
The Docker provider uses a non-root user, drops Linux capabilities, prevents privilege escalation and makes the root filesystem read-only by default. Network restrictions depend on the selected mode. Deny-network blocks egress, while an allow-mode host denylist is best-effort. Kubernetes network enforcement depends on a policy-capable network plugin.
Sandbox lifecycle matters too. release() drops warm compute but keeps the thread's volume and workspace. destroy() removes both the compute and the persisted workspace data, so a conversation that continues later loses its files.
The Access Control and Sandbox guides show how to configure each layer.
Delegate a taskCopy link to section: Delegate a task
A subagent is an agent that handles a bounded task for another agent. Our support assistant could delegate policy research, then use the findings to prepare its response.
The parent can register children by name and declare when each may run:
import { agent } from "@b4run/sdk"
import researcher from "./subagents/researcher/index.js"
import writer from "./subagents/writer/index.js"
export default agent({
model: "gpt-5-mini",
systemPrompt: "Coordinate research and writing for support requests.",
subagents: { researcher, writer },
delegation: {
default: "deny",
rules: {
researcher: { action: "allow" },
writer: { action: "approve", reason: "Approve draft generation." },
},
},
})Let's quickly review:
- First, we import two child descriptors. Each file exports its own
agent()declaration with the instructions and tools for its task. - Next, the keys in
subagentsname the children the parent can request. The delegation rules use the same names. - Finally,
researcheris allowed to run, while dispatchingwriterrequires approval. That approval covers starting the writer. Publishing or sending its output needs its own application controls.
The policy applies to this parent only. Allowing its research child doesn't authorize every child the researcher might call. Delegated runs keep separate saved state, and an approval requested by a child returns through the root conversation so the client can resume it there.
The current API requires a keyed subagents object. Array registration and old scalar resume bodies need migration. The Subagents guide covers the full configuration and resume behavior.
Remember information across conversationsCopy link to section: Remember information across conversations
Conversation state continues the current thread. Long-term memory keeps records for a later thread.
For example, a support assistant may need to remember a reviewed preference about how an organization receives reports. A route declares the shape of those records in memory.ts:
// src/app/support/memory.ts
import { defineMemory } from "@b4run/sdk"
import { z } from "zod"
export default defineMemory({
kind: "semantic",
scope: ["workspace", "route"],
identity: ["subject", "attribute"],
schema: z.object({
subject: z.string(),
attribute: z.string(),
value: z.string(),
}),
})Let's quickly review:
kind: "semantic"describes factual records.scopeplaces the collection in the workspace and route namespace. It keeps records apart but doesn't authenticate tenants.identitynames the fields that describe the same fact, such as an organization and its preferred report format.schemadeclares the data written with each record. Type generation carries this shape into the agent'sremembertool, and runtime validation checks writes again.
Review before recallCopy link to section: Review before recall
Agent-authored writes become candidates by default. A candidate is stored for review but left out of normal recall until approved. You can inspect and approve candidates through the CLI or the local Memory Inspector.
Why? An agent can misunderstand a conversation, and its interpretation shouldn't become trusted context for later answers until someone checks it.
The default SQLite store ranks recall by keywords, recency and confidence. Hybrid recall can also use vectors, which find related meaning when the wording differs. @b4run/memory-pgvector provides a shared Postgres option for the long-term collection.
Record events separatelyCopy link to section: Record events separately
Episodic memory records what happened during a run. The runtime recorder is optional and has retention limits. Two commands process older records:
b4 memory consolidatecompacts older episodes.b4 memory reflectderives insights for review.
Nothing runs these commands for you, so schedule them based on your application's needs and model cost. The Memory guide explains record types, review and recall. The Inspector guide covers the local development interface.
Connect a browser clientCopy link to section: Connect a browser client
A browser needs more than the agent's final answer. It may need to show text as it arrives, display tool activity and ask the user to approve an operation.
AG-UI is an event protocol for that interaction. B4.run maps runtime activity into AG-UI events, preserving tool-call ids so the browser can associate a result with the call that produced it. The reference web client uses CopilotKit.
The thread-scoped Agent Protocol API remains available for direct clients. A typical interaction is:
- Create a thread for the conversation.
- Start a run under that thread and read its result or stream.
- If execution pauses for approval, display the pending requests.
- Resume the root thread with decisions addressed to every pending interrupt id.
The ids matter when more than one operation is waiting. A single "yes" can't safely say which requests it answers.
Both client paths reach the same route code and configured runtime stores. See AG-UI and Web Clients and Dev Server for the request formats.
Deploy the applicationCopy link to section: Deploy the application
b4 build can generate a Node server and Dockerfile. b4 start runs the B4.run production runtime directly. Both support the HTTP APIs, middleware, tool rules, permissions and configured sandbox.
If the host has an ephemeral filesystem, or several instances need the same saved records, @b4run/postgres-storage stores checkpoints, threads and permission grants in Postgres. These are runtime records. Typed long-term memory uses its own store.
Note, cancellation and the one-run-per-thread guard stay process-local, so an application running several replicas needs to account for that.
The optional Hono target supports a narrower edge environment. It leaves out features that need a local filesystem or shell, and it is tested on local workerd, not a live Cloudflare deployment. B4.run at the edge explains that design and its limits.
Upgrade an existing applicationCopy link to section: Upgrade an existing application
Use Node.js 24 or later, and pin every direct @b4run/* dependency to the same release. The framework is pre-1.0, and 0.8 patch releases have included breaking API changes. To upgrade:
- Read the Upgrading guide and the intervening release notes.
- Update all direct B4.run dependencies together to the chosen published version.
- Run
pnpm exec b4 verifyto check the application. - Run the route tests and evals.
- Exercise the deployment target and approval flows the application actually uses.
For a new project, follow Getting Started. The starter gives you a working application with matching package versions.
ConclusionCopy link to section: Conclusion
An agent becomes useful inside an application when we can test its behavior, control its actions, keep the right context and connect it to users. These releases add more of those pieces around the route.
Start with the part your application needs next: a repeatable eval, an approval before a tool call or a memory collection you can review. Then test the whole interaction so you know what the framework handles and what's still up to your application code.