@guangnao/agent-os
v1.0.5
Published
AgentOS Kernel — a lightweight, type-safe agent operating system in TypeScript
Maintainers
Readme
@guangnao/agent-os
A microkernel operating system for LLM agents — not a framework, not a library.
The distinguishing line: the kernel owns execution and resources, and a process (an "agent") affects the world only through capability-mediated syscalls. That boundary — not the vocabulary — is what makes this a system.
- Metered by construction — every LLM/tool call is charged to a per-process budget (tokens, $, tool-calls, wall-clock, memory). Over budget → the kernel kills it.
- Least privilege — a process holds no ambient authority, only unforgeable capabilities you delegate. Attenuate and pass them like file descriptors.
- Let it crash — link/monitor + OTP-style supervisors restart flaky agents.
- Deterministic — record/replay driver I/O and a virtual clock reproduce a run byte-for-byte.
- Batteries included — LLM (OpenAI-compatible), tools, KV, cross-node gateway, worker-thread offload, and a WASM sandbox drivers; a ready-made ReAct agent.
Install
npm install @guangnao/agent-osESM-only, types bundled. Node ≥ 18.
Quick start — a real LLM agent in ~30 lines
A ReAct agent that thinks → calls a tool → observes → answers, with every LLM and tool call metered by the kernel. Point it at any OpenAI-compatible endpoint (DeepSeek below).
import { Kernel, llmDriver, toolDriver, openaiChat, reactAgent } from '@guangnao/agent-os'
import type { ProcessImage, ToolSchema, AgentResult, BootGrant } from '@guangnao/agent-os'
// 1. Tools are plain functions; the agent reaches them through a metered device.
const tools = {
calc: ({ op, a, b }: { op: string; a: number; b: number }) =>
({ result: op === 'mul' ? a * b : op === 'add' ? a + b : a - b }),
}
const schemas: ToolSchema[] = [{
name: 'calc', description: 'arithmetic on two numbers',
parameters: { type: 'object', required: ['op', 'a', 'b'], properties: {
op: { type: 'string', enum: ['add', 'sub', 'mul'] }, a: { type: 'number' }, b: { type: 'number' },
} },
}]
// 2. Boot a kernel; register the drivers (the only way out to the world).
const k = new Kernel()
k.registerDriver(llmDriver(openaiChat({
apiKey: process.env.DEEPSEEK_API_KEY!,
baseUrl: 'https://api.deepseek.com/v1',
model: 'deepseek-chat',
})))
k.registerDriver(toolDriver(tools))
// 3. init delegates the llm + tools devices to a ReAct agent and awaits its answer.
const init: ProcessImage = { name: 'init', async main(sys) {
const g = (await sys.recv<BootGrant>()).body
const { cap } = await sys.spawn(
reactAgent({ task: 'What is 23 * 19? Use calc, then give just the number.', tools: schemas }),
{ link: true, quota: { tokens: 200_000, usd: 1, toolCalls: 20, mem: 100_000 } },
)
await sys.send(cap, {}, { caps: [ // delegate caps like fds
sys.derive(g.devices['llm']!, ['open']),
sys.derive(g.devices['tools']!, ['open']),
sys.selfCap(),
] })
const r = (await sys.recv<AgentResult>()).body
console.log(r.answer, `(${r.steps} steps)`) // → 437 (2 steps)
} }
await k.boot(init, { grant: ['llm', 'tools'], quota: { tokens: 2_000_000, usd: 10, children: 4 } })A process is just { name, async main(sys, args) }. It holds no kernel object — only
sys, the syscall surface. Everything it can do (spawn, send, open a device, remember,
read an LLM) is a syscall, charged and capability-checked.
Architecture (microkernel, 3 rings)
Ring 2 · Userland — agents + services: pure main(sys) bodies, syscalls only
──────────────────── the syscall ABI (the only boundary)
Ring 1 · Services — drivers (LLM/tools/memory), supervisors, name service
────────────────────
Ring 0 · Kernel — PIDs · syscall dispatch · scheduler · accounting · cap table
· IPC switch · supervision mechanism · event log| Subsystem | What it is | Agent twist |
|---|---|---|
| Process | kernel-owned PCB + PID; the kernel runs/parks/kills it | not an object you new |
| Scheduler | cooperative, by reductions (syscall budget per quantum) + priority/EDF deadlines | schedules token/tool cost, not CPU time |
| Accounting | hierarchical (cgroup) quotas: tokens, tool calls, $, wall-clock, memory, children | parent's budget bounds its whole subtree |
| Capabilities | unforgeable handles; attenuate + delegate; no ambient authority | least-privilege, confused-deputy-proof |
| IPC | mailboxes + a switch; caps ride on messages (fd-passing) | the only way agents share data |
| Memory (MMU) | context window = paged address space; watermark evict → lossy summary → recall; OOM | pluggable keyword/vector recall |
| Supervision | link/monitor → Down (kernel) + restart strategies (userland) | OTP "let it crash" |
| Drivers | LLM/tools/memory/net behind one open/read/write | swap a vendor = swap a driver; metered at the chokepoint |
| Event log | every syscall/charge/fault, stamped | ps/top, audit, deterministic replay |
Driver standard library
| Driver | Purpose |
|---|---|
| llmDriver(chat) | LLM device; wrap any ChatFn |
| openaiChat(cfg) | a ChatFn for any OpenAI-compatible API (OpenAI, DeepSeek, …); pass onDelta on a request for SSE token streaming |
| toolDriver(tools) | expose plain functions as a metered tool device |
| kvDriver() | a shared key-value store device |
| gatewayDriver(forward) | cross-node IPC over a trusted channel (in-process / private link) |
| secureGatewayDriver(forward, key) | cross-node IPC over a hostile wire — HMAC-sealed, replay-protected |
| workerDriver(tasks) | offload CPU tasks to real worker_threads (isolated heap, another core) |
| wasmDriver(modules) | run untrusted compute in a zero-import WebAssembly sandbox |
Beyond the basics
- Deterministic record/replay — journal driver I/O; replay reproduces a run exactly.
- Virtual clock — discrete-event time; sleeps cost zero real time, traces are byte-identical.
- Snapshot / migration —
kernel.snapshot(pid)→ durable state (context + mailbox + budget);kernel.restore(snap, image)resumes it on any kernel (live migration across nodes). - Event-sourcing store —
MemoryStore/FileStorepersist every event + I/O; rebuild a replayable journal from disk. - Vector recall — context page-in via
KeywordRecaller(default) orVectorRecaller(embed). - Supervisors & name service —
supervisor({ strategy, children })(one_for_one/one_for_all/rest_for_one) and a capability broker.
Security & operations
- Authenticated transport — off-machine messages are HMAC-SHA256 sealed over a canonical
(to, nonce, body)encoding;kernel.ingress()verifies in constant time and drops replays. Capabilities never cross the wire — a verified peer can address a pid, never forge authority over one. - Crash recovery —
Kernel.replayFrom(store)rebuilds a kernel that replays persisted driver I/O, reproducing a run exactly with no live calls. The event-sourcing log is the recovery mechanism. - Backpressure —
quota.maxMailboxbounds an inbox; a full one parks senders until the consumer drains it (no drops, order preserved). - Metrics —
kernel.metrics()andkernel.metricsText()(Prometheus exposition) over the whole event history. - ABI versioning —
ABI_VERSIONis stamped into snapshots;restorerefuses an incompatible version rather than silently resuming mismatched state. - Tested for robustness — property tests (accounting conservation/atomicity, capability attenuation), adversarial tests (malicious-driver quota-kill), and chaos tests (spawn/kill churn) run on a seeded PRNG.
Performance
Single-core, cooperative kernel — these are one-core ceilings. Reproduce with npm run bench:
IPC round-trips ~530k /s send → recv → send → recv
Process spawn+exit ~135k /s spawn → notify → exit (O(caps-owned) revoke — flat under load)
Context switches ~2.1M /s sys.yield (O(1) dispatch — flat to thousands of procs)
Fan-in recv ~1.2M /s 32 senders → 1 deep mailbox (O(1) dequeue — flat under load)
Driver reads ~840k /s syscall + driver dispatch + accounting
Memory remember ~2.2M /s context write
Memory recall ~1.6M /s keyword page-in
Memory under paging ~900k /s write at the mem ceiling (O(1) amortized — flat vs resident size)
Parallel I/O ~16× 16 latency-bound reads: sys.readAll vs sequentialsys.readAll(handle, reqs) fires every request concurrently under a single park —
a ReAct turn's N tool calls become one network-bound wait, not N — while staying
fully metered and replay-deterministic (op ids are assigned in order).
(Apple M-series, Node 25; indicative, not a controlled benchmark.)
Real network + true isolation
- TCP transport —
serveTcp(env => kernel.ingress(env))+dialTcp(host, port)move HMAC-sealed envelopes over real sockets between kernels on different ports/machines. Authentic frames are delivered; tampered/forged ones are dropped at ingress. - Sandboxed agents —
sandbox(code)runs untrusted agent logic in a real worker thread (separate V8 isolate + heap). It holds no reference tosys, the cap table, or kernel memory; its only authority is a mediated syscall RPC through a trusted reference-monitor shim (allow-listed syscalls + the usual capability/quota checks). It can't read kernel memory, forge a capability, or call a syscall it wasn't granted.
Honest scope
Production-shaped but young — naming it straight:
- Distributed has authenticated envelopes + replay guard + a real TCP transport, but no built-in service discovery, failure detector, or partition handling yet.
- WASM sandbox confines compute; the agent-process isolation story is
sandbox()(worker-thread), not WASM-of-the-whole-agent. - Migration serializes a process's context + mailbox + budget, not its JS stack — so it migrates message-loop state machines, not arbitrary suspended call stacks.
- Preemption is cooperative. The kernel is one JS thread; every syscall charges a reduction and yields/faults at the quantum, so a loop over any syscall can't wedge it. But a syscall-free CPU loop (
for(;;){…}) can't be preempted from within the same thread — offload CPU-bound work toworkerDriver/wasmDriver(separate threads), the same way you'd never block Node's event loop.
Requirements
- Node ≥ 18, ESM (
"type": "module"or.mjs). - TypeScript optional but recommended — full types ship with the package.
License
MIT
