A reproduction of Cloudflare's Code Mode
where everything runs inside one BAML process: the model writes BAML, the server compiles it at
runtime with reflect.Package.compile, runs it in-process against typed connectors, and keeps a
durable execution log with approvals via abort-and-replay, replay-divergence detection, rollback, and
snippets. The React page in web/ is only a view: it receives every state change as an event over a
WebSocket (/ws) and sends commands back on it. No polling, no code in the browser.
model ──AgentStep{code}──▶ BAML server ──reflect.Package.compile(code, {host: current()})──▶ run(codemode)
│ │
│ codemode.crm.list_customers(plan = "pro") ◀── typed method ─┘
│ Runtime.call<T> → seq #n → replay | execute | throw PausedForApproval
│
web/ (Vite+React) ◀── events over /ws (or /api/events SSE) ── commands ──▶
./dev.sh # BAML server on :8787 + Vite on :5173Code generation (NextStep and FixCode in baml_src/agent.baml) runs on the local Claude Code CLI
with Sonnet (claude_code.ClaudeCodeClient), no API key needed. The prompt includes the BAML agent
skill from .claude/skills/baml-core/SKILL.md as the language reference, so keep it in sync with the
pinned toolchain via baml agent install. Inside its code the model can also ask for docs on any
stdlib path, codemode.describe("baml.fs"), which runs baml describe on the server.
Open http://localhost:5173 and pick a task.
Deep links: /?task=<url-encoded>&auto=approve runs a task on load and approves everything;
/?session=<id> reopens a finished run (ids are in the History tab); /?tab=tools|history picks the right-hand tab.
The page is deliberately plain and large for a projector: one story on the left, three reference tabs on the right.
The Env tab stores names and values in the BAML server (.codemode/env.json, mode 600). Values are
never sent to the browser or the model; only names are. Model code reads them with
codemode.env.get("GITHUB_TOKEN"). If the variable is not set, the execution pauses like an approval:
the story shows "The code needs GITHUB_TOKEN" with a password field, Save and continue stores it and
re-runs the code (earlier calls replay, env.get now succeeds). env.get uses the re-execute replay
policy so values never enter the durable log, and every stored value is replaced by «ENV:NAME» in
results, logs, and error messages before they reach the model or the page.
CODEMODE_ENV_IMPORT=GITHUB_TOKEN,OTHER ./dev.sh seeds the vault from the process environment.
- One tool, discovered by code. The model's first program calls
codemode.search("…")andcodemode.describe("billing");describerenders real BAML declarations withreflect.signatureover the connector's own methods (baml_src/codemode.baml). The catalog never enters the prompt. - The model writes BAML.
function run(codemode: host.Codemode) -> AnyType { … }: it can return a class it defines in the same source; the runtime calls.to_json()on whatever comes back. - Compile errors are repaired in place. If the source doesn't compile,
Runtime.run_passruns an inner loop: diagnostics →FixCode(a focused LLM function) → recompile, up to three attempts. Each attempt is anoticeevent (a toast in the UI) and aFixRecordon the execution with the broken source, the diagnostics, and the model's one-line explanation. The agent loop never sees the failure. - Snippets are typed packages. Save as snippet compiles the execution into a
reflect.Packagethat is passed into every later compile as a dependency. The model callsenterprise_names.run(codemode)with real types,describe("enterprise_names")renders its functions fromPackage.functions(), and the Tools tab lists them next to the connector methods. - Tools are listed by reflection. The Tools tab is built from
reflect.signatureon each connector method (name, parameters, defaults, return type,///docstring) plus each snippet package's functions. Tools the model has looked up in the current session are marked found. - Approvals = abort + replay.
email.send,update_plan,issue_refundneed approval. The method body reachesRuntime.call<T>, which records the entry as Pending and throwsPausedForApproval; the pass unwinds. Approve → the same code runs again; earlier calls are answered from the log (dimmed, "answered from log"), the approved call executes, the code continues. - Deterministic replay. Same seq must see the same connector/method/args (structural
==), orReplayDivergence.codemode.step("name", () -> { … })records nondeterministic values. - Rollback. Execution log → Roll back walks applied calls in reverse and calls each connector's
revert. Watch Live data change back. - Panics, errors, limits. Out-of-bounds in generated code is caught via
spawn+all_settled; uncaught connector errors end the execution; history is pruned at 50;.codemode/*.jsonsurvives restarts.
| file | role |
|---|---|
baml_src/agent.baml |
NextStep and FixCode LLM functions, the shared codemode_surface / baml_cheatsheet prompt text, the agent loop on a detached green thread, sessions, WebSocket/SSE fan-out |
baml_src/codemode.baml |
Codemode, the object handed to generated code: one field per connector, search/describe/step/run/publish; compile_run and the describe renderers |
baml_src/runtime.baml |
Runtime: executions, LogEntry with seq/state/pass, run_pass, call<T> (the interception point), approve/reject/rollback, snippets, prune/expire, persistence |
baml_src/connectors/connector.baml |
The Connector interface, Tool + tool(self.method, approval = …, revert = …), and call<T>, the one line every method body goes through |
baml_src/connectors/*.baml |
One file per connector: its result classes, state, seed data, and the class whose typed methods are the API (crm, billing, email, github, env) |
baml_src/server.baml |
HTTP routes over baml.http.Server, typed StateView |
web/src/App.tsx |
timeline derived from server state, approvals, Live data / Execution log / Snippets / Tools |
One file in baml_src/connectors/, then one field, one entry in all(), and one line in bind() on
Codemode. A connector is a single class: its typed methods are its API. Each body wraps its work in
call(self.ctx, self.method, args, () -> { … }) so the runtime can log, replay, pause, and roll it back,
and tools() declares policy once, next to the method:
class Crm {
customers: Customer[],
ctx: Ctx?,
/// Change a customer's plan. Requires user approval. Reversible.
function update_plan(self, id: string, plan: string) -> PlanChange throws PausedForApproval | ReplayDivergence | ConnectorError {
call<PlanChange>(self.ctx, self.update_plan, j({ "id": id, "plan": plan }), () -> {
let c = self.find(id) ?? throw err(`no customer with id ${id}`);
let previous = c.plan;
c.plan = as_plan(plan) ?? throw err(`unknown plan ${plan}`);
PlanChange { id: id, previous_plan: previous, plan: c.plan }
})
}
function bind(self, rt: Runtime, exec: Execution) -> Crm throws never { Crm { customers: self.customers, ctx: bind_ctx(self, rt, exec) } }
implements Connector {
function name(self) -> string throws never { "crm" }
function description(self) -> string throws never { "Customer records." }
function tools(self) -> Tool[] throws never {
[tool(self.update_plan, approval = true, revert = (args, result) -> { … })]
}
}
}reflect.signature(self.update_plan) supplies the name, docstring, and parameter and return types to
search, describe, and the Tools tab, so nothing about a method is ever written out as a string.
GET /ws upgrades to a WebSocket. The server sends {type:"snapshot", state} on connect, then one event
per change: {type:"session", session}, {type:"execution", execution, world}, {type:"snippets", snippets},
and {type:"result", command, result} in reply to a command. Commands are JSON frames:
{type:"start", task}, {type:"approve"|"reject"|"rollback", execution_id},
{type:"save_snippet", execution_id, name, description}, {type:"run_code", code, task}.
node scripts/ws-client.mjs "task" drives a whole session from the terminal this way;
python3 scripts/session.py "task" does the same over HTTP, and python3 scripts/direct.py runs the
hand-written runtime scenarios (no model). CODEMODE_API=http://127.0.0.1:8788 points the scripts and
pnpm dev at another server.
GET /api/events streams the same events as Server-Sent Events with commands over the POST routes below.
The page uses SSE by default; VITE_TRY_WS=1 pnpm dev makes it try the WebSocket first (expect Vite to log
"ws proxy error: write EPIPE" while the BAML bug below stands; the page falls back to SSE).
Known issue (0.19.0, 0.19.1-nightly.20260911.a, 0.18.1-nightly.20260909.a; feedback e1911bc9): the websocket handler runs and
returns its WsAccept closure exactly as the stdlib docs show, but the engine answers 500
"websocket handler failed" before the 101. The SSE path is what the demo runs on today; the WebSocket
code is in place for when a nightly fixes it. Minimal repro: a serve(handler, websocket = (req) -> { (socket) -> { } }) and any client upgrade.
GET /api/events Server-Sent Events, same events as /ws
GET /api/state everything the UI renders
POST /api/sessions {task} start the agent loop (background), returns the session
POST /api/executions {code, task} run BAML directly, no model (synchronous outcome)
POST /api/executions/:id/approve | reject | rollback
POST /api/snippets {name, description, execution_id} save an execution as a tool by hand
GET /api/env names of stored variables (values are write-only)
PUT /api/env/:name {value} · DELETE /api/env/:name (also `set_env` / `delete_env` WebSocket commands)
GET /api/tools every tool (connector methods + saved tools), by reflection
GET /api/tools/:name one saved tool: source, functions, executions that use it
DELETE /api/tools/:name remove a saved tool (also the `delete_tool` WebSocket command)
POST /api/maintenance/expire {max_age_seconds} · /api/maintenance/prune
The project is pinned to 0.19.0 in baml.toml ([toolchain] version), so your global nightly
selector is free to move. Everything below was verified on 0.19.0 and 0.19.1-nightly.20260911.a.
The WebSocket accept bug is filed as BAML feedback e1911bc9 (repro in repro/ws_accept.baml).
- A package used as a
reflect.Package.compiledependency cannot containtypealiases, and its methods cannot have string-literal parameter defaults. Unions are inlined; optional params default tonull. - Extract the entry point with
get_function<reflect.AnyFunction<Returns = json, Throws = unknown>>and call it withreflect.call_any; function-type literals don't parse in generic arguments. - Generated code is isolated as a package but not as a security boundary: it can still reach the BAML stdlib. Treat this as the "pretend sandbox".
- Started from inside a Claude Code session?
dev.shunsets the nested-session env vars so the CLI fallback can run.