A tiny immutable kernel.
A self-authored userland.
An agent whose mind is a directory tree and a SQLite file, whose only action primitive is “run TypeScript in a persistent kernel,” and whose prompts, tools, and subagents are files it writes itself. Built from the primitives up: sandbox → filesystem → evaluator → code-mode → memory → self-improvement.
Grounded in arXiv 2504.01990 — Advances & Challenges in Foundation Agents · target runtime: Bun 1.4 · TypeScript · Mastra + Vercel AI SDK + zod · motion.dev
The Substrate: two planes, one boundary
Everything starts with a sandbox with a persistent volume — and one architectural decision that buys most of the safety story for free: the sandbox holds zero secrets and zero direct model access. Model API calls, credentials, and budget metering live on a controlled server outside. Inside, they appear only as injected functions — capability references the kernel chose to hand in, revocable and metered at the choke point.
This is capability security: the agent doesn’t have permissions, it holds references. Exfiltration, budget enforcement, and the promotion gate all collapse into this single boundary.
The proxy itself is thin: it speaks the Vercel AI SDK provider layer, so llm() routes model strings to any provider, and generateObject hands back zod-validated structure to whoever asks — scorer verdicts, reflection diffs, subagent proposals all arrive already typed. Mastra is built on the same SDK, so one abstraction runs unbroken from sandbox code to metered call.
A Mind on Disk
The agent’s home directory is its mind. SQLite holds what needs querying (traces, facts, registries, scores). The filesystem holds what needs executing or diffing (prompts, tools, agent definitions). Blobs hold what’s big, content-addressed. Nothing lives in a vector database you can’t cat.
bun:sqlite being synchronous is load-bearing: a memory lookup costs the agent one inline line of code, no async ceremony inside generated snippets.
Does synchronous hold at scale? Measured (2M-row trace, WAL, Apple silicon — bun run bench-sqlite.ts in the repo): indexed point reads run at 6µs each (158k/s), single-writer inserts at 940k/s inside a transaction. At those latencies async buys nothing — SQLite is in-process, there is no network to overlap, and a real async driver adds a thread-pool round trip per query plus suspension points that break a snippet’s atomicity. Blocking is harmless by construction: a session kernel is one thought at a time, so a 6µs read steals time from nobody. The discipline that keeps this true: WAL + busy_timeout, the kernel as the single writer, accessors that ship only indexed plans with LIMITs, blobs outside the DB — and anything heavy (a full-scan aggregate over 2M rows costs ~500ms) is exiled to the sleep phase, where consolidation doubles as ANALYZE and trace archival. Forgetting is also index maintenance. (At fleet scale — chapter 08 — parallel tasks run on cells with their own local db and batch-merge home, so the single writer never becomes a shared bottleneck.)
agent.db — prebuilt, migrations are human-approved deploys events(id, ts, kind, actor, payload·json, task_id, cost) ← the trace: source of truth facts(id, subject, body, provenance, score, last_access) ← semantic LTM entities / relations ← generic graph, JSON columns flex tool_registry(name, sig, version, tests, uses, success_rate) scores(run_id, scorer, value, reason, cost) ← Mastra scorer output blackboard(task_id, agent, proposal·json, status) ← subagent proposals blobs(hash, path, bytes, mime, origin) ← served via Bun.file streams; cold-tiered to S3 (built-in client)
The Session Kernel
The session kernel is what makes code-mode real: one long-lived Bun process per session that executes every snippet the model writes and keeps the results alive between turns. Its shape is Jupyter’s, not a terminal’s. A REPL is a UI for a human — prompts, ANSI color, output pretty-printed for eyes; a kernel is a protocol for a machine — snippets in over RPC, structured envelopes out (ok, result preview, captured stdout, bindings delta, duration), introspection as first-class messages. About 400 lines, and they’re lines worth owning.
Inside, snippets run against a persistent vm.createContext namespace. The reason it’s vm and not a raw eval loop: top-level bindings land as enumerable properties of the context object, and that one property makes three hard features free — the whos digest (the model always sees what is in working memory without the data entering context), snapshots (serialize the enumerable subset to disk), and an honest reload. Note what vm is not doing here: security. Isolation belongs to the sandbox boundary of chapter 01, so the context never has to be trusted as a jail — it only has to be a namespace.
How it’s built: crash-only, protocol-first
Kernel logic is fixed; the executor is a swappable slot. The slot is a three-method interface — eval, bindings, dispose — so where code runs is policy, not architecture. The default VmExecutor runs in-process: JIT-native speed, zero serialization between snippets and bindings, synchronous bun:sqlite one await-free line away. An IsolateExecutor (secure-exec’s V8 isolates, deny-by-default grants) runs quarantine tests for candidate tools. A future remote executor changes nothing above the interface.
The most elegant move is what we don’t build: a safe interrupt. Bun’s vm timeout options are unreliable, so instead of fighting for graceful cancellation, the kernel is crash-only — a wall-clock watchdog, then SIGKILL. That’s safe because recovery is first-class: fresh context → replay the prelude → restore the snapshot → replay the code log. The interrupt of last resort and the recovery path are the same mechanism, so the kernel has no cancellation states to get wrong.
One honesty rule anchors persistence: full JS state cannot be serialized — closures, sockets, live imports. So the kernel never pretends. Serializable bindings snapshot to disk at checkpoints; everything else is reconstructed by replaying the code log from the trace. The snapshot is an optimization; the replay is the guarantee — episodic memory and session persistence turn out to be the same mechanism.
Evaluated and set aside on the way here: driving bun repl programmatically (a TTY is a UI, not a protocol — you’d be screen-scraping ANSI and guessing prompt boundaries); Python + ipykernel (the right protocol shape — Jupyter solved this years ago — but the wrong language: it forfeits the typechecker as free verification, zod-typed tools, and synchronous bun:sqlite); and in-process virtual OSes like agentOS (they exist for people who don’t own a sandbox — we do; bookmarked for subagent fleets). secure-exec survived, as the quarantine executor.
// wire protocol — Bun.serve on a unix socket; every reply is a machine envelope, never a TTY → { id, op: "exec", code } ← { id, ok, resultPreview, stdout[], bindingsDelta, ms } → { op: "whos" } ← [{ name, type, bytes, preview }] → { op: "snapshot" } // interrupt has no op: watchdog → SIGKILL → replay interface Executor { // kernel logic never changes when this swaps eval(js: string): Promise<Result> bindings(): BindingInfo[] dispose(): void } new VmExecutor(preludeCtx) // default: same-process, sync sqlite, zero serialization new IsolateExecutor(grants) // quarantine: deny-by-default, least-privilege grants
Session continuity: variables outlive the message
The kernel’s lifetime is the session, not the turn — so tool results computed during one user message are simply still there when the next message arrives, minutes or hours later. The context compiler includes the whos digest, the model sees har · HarEntry[1204] · 48.2 MB, and writes har.filter(…) directly: no reload, no recomputation, no re-tokenization.
If the kernel died in between (crash, watchdog, host restart, deliberate hibernation of idle sessions), the reload path runs before the turn starts — with one crucial rule: replay must not re-fire side effects. The trace already records every tool call’s result for evaluation, so replay runs in memoized mode — tool invocations are substituted with their recorded results from the events table instead of re-executing (the Temporal trick). Recomputing a HAR parse is harmless; re-sending a Slack message is not. Memoized replay makes reconstruction deterministic, side-effect-free, and free of API cost — and it falls out of infrastructure the trace was already providing. Sessions become durable objects: killable, migratable, resumable days later with the same variables.
Code-Mode
The survey validates this three separate times: skills are best stored as code (Voyager, JARVIS-1), workflows are most expressive as code (AFLOW, ADAS), and world models can be written as code (WorldCoder). So the agent gets exactly one action primitive — evaluate TypeScript in the kernel — and tool-calling, tool-creation, planning, orchestration, and memory access all become libraries, not modules.
We don’t need Symbolica’s type theory literally: zod schemas on Mastra tools give both halves — runtime validation at the call boundary, static types via z.infer. Generated code is typechecked before execution, which catches hallucinated APIs at zero model cost. The typechecker is the cheapest evaluator in the whole system.
The Prelude
What the agent’s code can see is decided by the prelude — the globals loaded into the kernel context at session start. It has two strictly different layers. The kernel prelude is host-injected and immutable: RPC stubs to the control plane. The userland prelude is TypeScript files on disk — seeded with core helpers, then grown and edited by the agent itself through the promotion gate.
Prelude replay is also the reload mechanism from chapter 03 — one design, two jobs.
Memory: the paper’s taxonomy, made of files
The survey’s memory hierarchy maps one-to-one onto this substrate — and the elegant row is procedural memory: it isn’t a table about skills, it is the /tools directory. Import path = memory address.
Two rhythms run the lifecycle. Online, during a task: events append to the trace, big outputs go to blobs, retrieval is FTS5 + recency/importance scoring — and since retrieval is a userland helper, the agent can rewrite its own retrieval. Offline, a sleep phase: compress episodes into insights, promote repeated observations into facts, decay what’s unused, dedupe tools, and draft the overlay diffs that then face chapter 09’s promotion law — sleep proposes; only the law lands.
Sleep has a trigger, and it is the system’s one emotion: tiredness. One thing tiredness is not: context-window fullness. The window is auto-managed housekeeping — the compiler pages, compacts, and substitutes whos digests for data continuously, routine even across tens of millions of tokens — and it is never a reason to sleep. Tiredness measures pending change to the agent itself: trace events not yet reflected into insights, overlay diffs waiting to land, a tool library due for curation — consolidation debt that accumulates over days of contextual information. That debt is one SQL query over agent.db, and it rises like sleep pressure; past a threshold, sleep is scheduled. No other affect exists in the system.
Tools That Build Themselves
A tool is a Mastra createTool() with zod input/output schemas, re-exposed in code-mode as a plain typed function. The interesting part is the lifecycle — lifted from Alita and ToolMaker, the best-specified loops in the literature: the agent detects a capability gap, drafts a spec, writes the module, generates its own tests (plain bun test files — the gate runs the runtime’s native test runner, no invented harness), and runs them quarantined. Only a pass crosses the promotion gate — which lives on the trusted side, so self-written code can never approve itself.
The registry row is what makes the library curatable: the sleep phase prunes what never gets used or keeps failing — Voyager’s skill library with garbage collection. Note the division of labor with chapter 09: tests are the entry bar — correctness at birth — while whether a version stays is the promotion law’s call, fed by this registry’s live stats.
createTool({
id: "parseHarFile",
inputSchema: z.object({ path: z.string() }),
outputSchema: z.object({ requests: z.array(ReqSummary) }),
execute: async ({ path }) => { … }
})
// runtime re-exposes it in the kernel context as:
const har = await tools.parseHarFile({ path: "trace.har" })
// zod validates at the boundary · z.infer feeds the .d.ts the model reads
Subagents & the Blackboard
A subagent needs no new machinery: it’s a TS file in /agents exporting a Mastra Agent — instructions, model, a tool allowlist, and a budget carved from its parent’s. Spawning is a function call. Children run the same kernel loop against the same agent.db (WAL handles concurrent readers) in a scoped workspace.
Two findings from the survey shape the orchestration hard. First: child outputs are proposals, not actions — they write to the blackboard table; only the parent commits side effects. Second: the scaling paradox — coordination overhead grows super-linearly while contribution grows linearly, so every task has a small optimal team size. Start dictatorial-hierarchical, hand off artifacts through files, save debate topologies for later experiments.
Fleet scale: thousands of tasks, one mind
The fleet model in one sentence: the mind is a single-writer organism; the hands are unlimited. One home node owns the truth — agent.db, the identity repo, the promotion gate, the sleep phase. A task spawns a cell: a sandbox on any machine, running the same kernel, with a git checkout of userland at a pinned commit (identity, tools, helpers travel as files, because the mind is files), a read snapshot of facts, and its own local task.db — every cell keeps the 6µs synchronous reads, because sync-SQLite-per-process was never a shared resource.
Cells never write home directly — the blackboard discipline generalizes to the fleet. A cell’s trace is an append-only log with ULID ids, batch-merged into the home trace at checkpoints: one transaction per batch is the 940k-inserts/s regime, so ten thousand cells emitting a few events a second consume roughly 1% of one node’s write budget. Results return as proposals for the coordinator; sleep, consolidation, and the promotion law run only at home, against the merged truth; the stateless proxy meters every cell’s budget because every llm() already crosses it. Nothing here is new machinery — per-cell isolation is chapter 01’s boundary, distribution is git clone, merging is the trace’s own append-only semantics. Horizontal scale falls out of decisions made for other reasons.
The Improvement Loop
The survey’s four optimization spaces — prompts, workflows, tools, whole-agent — all become file edits here, and every edit follows one lifecycle: reflection proposes a diff, scorers measure what it actually does, arithmetic decides whether it stays. Three roles, never collapsed into one — the rest of this chapter is just those three roles made precise.
Measurement lives in the sandbox: Mastra scorers run through the same llm() stub as everything else, sampled live during tasks and in batch over the trace during sleep. Their numbers persist next to memory, so “is version B better than version A?” compresses into one query — SELECT version, avg(score), avg(cost) FROM scores JOIN events — performance × cost × latency, the paper’s exact objective triple, in a single join.
The judge is math: propose → measure → decide
Why must the decider be arithmetic? Because an LLM judging an LLM’s self-edits is circular — a judge made of language can be flattered, gamed, or drifted by the very thing it judges. So approval power is removed from language entirely: the proposer is free to be maximally creative because it holds none of it, the sensors only observe, and the verdict is computed on the trusted side from numbers the agent cannot forge.
Mastra’s scorer pipeline is built for exactly this split: .analyze() is a prompt object where the judge fills a rubric — a zod-schema’d checklist of observable facts (does the output cite the trace? how many claims verified? did scope creep?) — and .generateScore() is a plain function: weighted arithmetic over the checklist. The LLM never emits a score; it emits observations, and the number falls out of code. Around these noisy sensors sit deterministic anchors no rhetoric can move — bun test pass rate, typecheck, cost, latency — plus holdout rubrics the reflection pass never sees, so it can’t train to the test.
Promotion is then a sequential probability ratio test (Wald): every scored task nudges a log-likelihood ratio; crossing the upper boundary promotes, the lower rejects, in between the only legal move is collect more evidence — with false-promote and false-reject rates bounded by construction. On top: Pareto non-inferiority (score must rise and cost must stay within margin) and hard invariants (safety scorers pinned at 1.0, fixed canary tasks green). And the agent can’t lie to any of it, structurally: scores are written only by the kernel-owned scorer path, the trace is append-only and hash-chained, and the gate recomputes the SQL itself — the agent never presents its own numbers.
Self-healing follows for free, and the recovery ladder is the local git history itself. Every promotion is a commit in the userland repo; the scores table keys on the commit hash, so “best-measured version” is one join — argmax(avg score) GROUP BY commit — and healing is git checkout <that hash>. EWMA/CUSUM drift detectors watch the live score stream as setpoints; a violation triggers the checkout automatically. No judgment involved, only arithmetic plus a commit log: the system’s steady state is “best measured commit, everywhere,” every deviation resolves back toward it, and because history is append-only the agent can experiment freely — there is always a measured rung to climb back to.
createScorer({ judge: { model, instructions: OVERLAY_RUBRIC } })
.analyze({ outputSchema: z.object({ citesTrace: z.boolean(),
claimsVerified: z.number(), scopeCreep: z.boolean() }),
createPrompt: ({ run }) => `fill the checklist for: ${run.output}` }) // LLM emits facts, never a score
.generateScore(({ results: r }) => // the number falls out of code
0.5 * r.analyzeStepResult.claimsVerified
+ 0.3 * (r.analyzeStepResult.citesTrace ? 1 : 0)
- 0.2 * (r.analyzeStepResult.scopeCreep ? 1 : 0))
// gate, trusted side: SPRT over these numbers + cost non-inferiority + invariants.
// violation later? EWMA detector → git revert to argmax(avg score) — healing is a query.
The ratchet: months of evolution, same law
One promotion cycle shows the law; the point of the system is what the law compounds into. Below, months compress into seconds — but nothing is scripted: every candidate the reflection loop proposes gets a true quality drawn from a realistic distribution (most ideas are mediocre, some are bad), every verdict is a real SPRT run against the current baseline, promotions re-anchor the baseline at the candidate’s measured reality, latent regressions fire later and get caught by the monitor, and gains shrink as quality approaches the ceiling. Quality climbs because most proposals die at the gate — the ratchet only turns on proven gains, and it can always fall back to the last good commit.
Budgets: tokens and time
Two budgets, enforced in two places. Tokens are metered at the LLM proxy — the one gate every model call already crosses; a subagent’s allowance is a sub-allocation of its parent’s. Wall-clock is enforced by the session kernel. Both are exposed read-only inside the runtime, so the agent can adapt its depth to what’s left — the paper’s “inference-time resource allocation” problem, solved cheaply.
Safety Rails
A self-modifying agent sits near the survey’s “autonomous improvement” red line, so the rails are structural, not vibes. Four of them: provenance on every memory row (the cited attacks poison retrieval with ~10 documents — so web-sourced rows render as data, never instructions, enforced by the context compiler, not the prompt); quarantine-then-promote for all self-written code; least privilege as allowlists on tools and subagents; and an immutable kernel prompt — overlays learn, the constitution doesn’t move, and irreversible external actions gate on the human regardless of what any overlay says.
One Complete Turn
Every layer above, in the order it fires. The only asynchronous branch is scoring — quality evaluation rides along, off the critical path, sampled per Mastra’s rates.
The Irreducible Kernel
Everything else on this page is seeded userland the agent can grow. The trusted core — the part that must be hand-built, reviewed, and kept small — is five components. Get the trace schema and the promotion gate right, and the agent bootstraps the rest of itself: that is the survey’s entire Part II thesis landing in a couple thousand lines of TypeScript.
llm proxy + budget meter
Control plane. All model calls, all creds, token metering per session and subagent.
session kernel
Protocol server + swappable executor (vm default, isolate quarantine), declaration hoisting, whos digest, crash-only interrupt, snapshot + trace replay.
context compiler
Deterministic per-turn assembly: kernel prompt → overlays → whos → retrieved memory (provenance-tagged) → tool .d.ts → budget state.
trace logger
Append-only events table designed for evaluation, not just logging. The most important schema in the system.
promotion gate + schema owner
Trusted-side verdicts on self-written tools, helpers, subagents: runs the tests, the SPRT law, and the invariants. DDL is a human deploy.
Four of the five, running on this page
The session kernel runs live in chapter 03, and the promotion gate in chapter 07 really promotes into it. The two below close the set: the trace logger has been recording your actual interactions with this page since you arrived, and the context compiler assembles a real prompt from the page’s real state — live whos, the tools you actually promoted, the real ledger. Only the llm proxy’s completions are canned (no keys in a browser); its metering and refusal are real.
bun:sqlite sync reads — one await-free line in generated code · measured 6µs/read at 2M rows Bun.Transpiler µs type-strip inside the kernel's hoist pass, no build step Bun.serve kernel RPC on a unix socket; the same server streams blobs via Bun.file — lazy, zero-buffer bun test self-generated tool tests run on the native runner — the promotion gate invents nothing Bun Shell ($) tools that wrap CLIs stay cross-platform one-liners Bun.spawn a subagent kernel is one cheap process; built-in S3 client cold-tiers old blobs bun build --compile kernel + prelude ship as a single binary baked into the sandbox image
The paper’s modules, accounted for
The survey’s Foundation Agent (Definition 2) names the modules below. Each is either owned by a component on this page, dissolved into code-mode, or skipped on purpose — nothing is unaccounted for.
| paper module | where it lives here | status |
|---|---|---|
| perception | the context compiler — whos, retrieved memory, budget assembled each turn; retrieval conditioned on the trace is the paper’s “perception guided by prior mental state” | owned |
| memory | ch 02 + 06 — six tiers over sqlite / fs / blobs, lifecycle via the sleep phase | owned |
| world model | dry-run mode on tools + simulators the agent writes as code (WorldCoder pattern) | dissolved |
| reasoning · planning | the model, plus plan() drafting code it doesn’t execute yet — planning as internal action | owned |
| action | one primitive: typed TypeScript in the kernel; skills as code | owned |
| reward | internalized as scores × cost in agent.db — not an environment scalar | owned |
| learning | online (bindings, reflection) + offline (sleep consolidation) — the paper’s hybrid rhythm | owned |
| self-evolution | four optimization spaces as file edits behind the promotion gate | owned |
| multi-agent | blackboard, proposals, coordination-operator parent, optimal team size | owned |
| safety | two planes, provenance, quarantine, immutable constitution | owned |
| emotion | exactly one: tiredness — consolidation debt (unreflected trace, pending overlay diffs) rising like sleep pressure; past a threshold it schedules the sleep phase, which consolidates memory and drafts the system-prompt updates that still face the promotion law. Never context-window fullness — that’s auto-managed housekeeping at any scale | owned · one signal |
Open threads, in attack order
- The trace/outcome schema. How events join against Mastra’s scores table — design them as one schema, or the improvement loop stays decorative.
- The context compiler contract. Exactly what the per-turn digest contains and in what order; it’s where memory “utilization” actually happens — and it should eventually be userland too, behind the same gate.
- Calibrating the promotion law. The protocol itself is now specified (tests → SPRT → non-inferiority → invariants, chapter 09); what remains is per-class calibration — how large a gain each artifact type must show (H₁ gaps), cost margins, and the holdout-rubric rotation policy.