kernel
The syscall table. An ambient global, bound at load(), process-lifetime — this one
object is the entire vocabulary a cognet has for touching the world.
Nothing here is wake-scoped. What a wake delivers arrives as loop
arguments instead.
type KernelAbi = {
// Emission. Unmediated — the kernel commits and forwards, never refuses.
output<K extends keyof AxonOutputEvent>(type: K, data: AxonOutputEvent[K]): Promise<void>
// Inference, by role. See kernel.engine.
engine: KernelEngines
// Execution. Mediated, policy-gated, NEVER rejects.
run(code: string): Promise<AxonRunResult>
run(code: string[]): Promise<AxonRunResult[]> // concurrent, results in order
// Reading the world the kernel guards.
scope(): AxonScope // what the capsule can execute right now
base(): Promise<string> // the user's rendered identity. "" when none, never undefined
// Telemetry. cognet:* only — a cognet cannot forge kernel events.
emit<K extends keyof CognetEventMap>(type: K, data: CognetEventMap[K]): void
fault(input: { code: string; message: string; excerpt?: string }): Promise<void>
// Persistence. See kernel.store.
store: KernelStore
knowledge: KernelKnowledge
// Rhythm. Continuous cognets only.
wake(): Promise<number>
clock(): KernelClock
}
type KernelClock = {
/** Wakes admitted since boot. Monotonic, process-lifetime. */
wakes: number
}
That is the whole surface. There is no escape hatch, and no verb below carries a
signal — cancellation is applied by the kernel unconditionally.
output — emission
await kernel.output("cognet:output:text", { channel, content: "done" })
| Type | Payload carries |
|---|---|
cognet:output:text | content, optional format (json / markdown) |
cognet:output:audio | ref, optional transcript, durationMs |
cognet:output:visual | ref, kind: "image" | "video", optional caption |
cognet:output:vector | values: number[], optional unit/units/labels/profile |
Every payload takes a channel and an optional chunk correlation group.
Unmediated at the call site: the cognet already holds the full content and nothing external can fail it, so there is nothing to refuse — writing to your own stdout cannot itself harm anything. The kernel commits it durably to the session's one log and forwards it live. Redaction, if a host wants it, happens downstream on the delivery path, never as a gate this call passes through.
There is deliberately no push or delta channel. A temporally-extended emission — streaming
speech, a progressive result — is expressed through output() itself using the chunking
standard: correlated entries closed by a final marker. Chunks are ordinary committed
entries, so they reach every observer through the same pipeline as any other fact.
run — execution
const result = await kernel.run(code)
const results = await kernel.run([blockA, blockB]) // concurrent, order preserved
type AxonRunResult = {
ok: boolean
/** The block's completion value when ok. Undefined on failure. */
value?: unknown
/** console.* from this block, in order, one entry per call. */
stdout: string[]
/** Top-level bindings the block declared — what a template interpolates against. */
scope: CapsuleScope
error?: { kind: "timeout" | "interrupt" | "exception"; message: string }
}
A mediated request — the cognet does not know the outcome until the capsule responds,
so it is policy-gated. It never rejects: success and failure are both ordinary values.
Timeout, abort and exception collapse into one error.kind rather than three control
flows to catch and re-discriminate.
stdout is captured server-side and returned inline. There is no callback to wire: the
kernel already forwards the capsule's own event stream onto the bus, and a cognet folding
console output into a committed result just reads it off the result.
scope is a diff of the sandbox's globals across the submission — bindings that crossed
in values, and by name in unavailable those that could not (function, circular,
unserializable, oversized). Empty on failure and empty for a block that declared
nothing, never absent, so a caller never distinguishes "no scope" from "scope unavailable".
An array runs every block concurrently and returns results in the same order. The common "await several tool calls before answering" case needs no manual fan-out.
The kernel commits cognet:action:typescript and cognet:action:result itself, the
moment each block settles. The cognet reads the value for its own control flow and writes
nothing.
scope — executable reality
const scope = kernel.scope()
The complete TypeScript surface the current capsule incarnation implements: modules,
each with named members carrying a declaration and optional jsdoc.
scope.unavailable is the half that matters. A module whose tools failed to compile
contributes nothing to modules — correct, because the model must never be told about a
tool the capsule cannot load — but that absence is silent, and an agent that cannot see
its own loss answers as though the capability never existed. Rendering unavailable lets
it say "I cannot search arXiv right now" instead of guessing. Empty in the normal case.
The kernel reports what is executable. Whether and how that enters model context is the cognet's decision alone.
base — the user's identity contract
const identity = await kernel.base() // "" when none declared, never undefined
The agent's rendered boot.vue. Kernel-mediated because it is the user's contract, not
cognet strategy: a swapped brain decides where the base context sits in its rendering,
never what it says. The seam normalizes, so callers never branch on absence.
emit — narrate your own world
kernel.emit("cognet:phase:start", { tick, phase })
Fire-and-forget for the cognet, durable in the machine: committed to the log and forwarded to the runtime bus as flame-graph material. Never rendered to the user.
Typed against CognetEventMap — cognet:* and nothing else. A cognet narrates its own
world; it can never forge kernel machinery events. In practice you rarely call this
directly: phase and system emit the clock families for you.
fault — the model broke your grammar
await kernel.fault({
code: "UNDECLARED_BINDING",
message: "interpolated {total}, which the script never declared",
excerpt,
})
Separate from emit because the outcome is not telemetry: the kernel commits it as
axon:system:message, so the next tick's rendered context carries it in a <system> block
and the model reads its own violation and corrects.
That is a system fact, and a cognet may not forge one directly — so it describes the fault and the kernel writes it.
Why the verb exists at all: the runtime detects violations visible in the token stream (an unclosed block), but a cognet that renders the model's output detects its own class of them — an interpolation naming a binding the script never declared. Without this, the only way to make such a fault visible was to speak it as the agent's own message, which puts a diagnostic in the agent's voice and shows the user machinery they cannot act on.
Deliberately not user-facing. Hosts hide these, so a violation is a retry the model performs rather than an error the user watches it make.
wake — the brain's own rhythm
// plugins/clock.ts
export default definePlugin(({ hooks }) => {
hooks.on("boot", () => {
setInterval(() => void kernel.wake(), 1000 / 30)
})
})
Continuous cognets only. An invocation cognet is woken by a stimulus arriving; waking itself would be a second, contradictory trigger.
The body emits stimuli and never decides when the brain looks at them: how fast frames arrive is a property of a sensor, how often it is worth thinking about them is a property of a mind. A body that drove a specific brain would have to be rewritten when the brain changed — exactly the coupling the split exists to prevent — and it has no answer under composition, where two sensors at 31Hz and 60Hz cannot both be "the" tick rate.
Resolves with the wake's ordinal as soon as it is admitted, never when it completes. A driver that awaited completion would serialise the overlap continuous mode exists to allow.
Named wake, not tick, because that is what it does: it invokes the loop. A tick is one
iteration inside a wake.
clock — a snapshot of that rhythm
const { wakes } = kernel.clock()
if (wakes % 4 === 0) { /* the slow path, every fourth wake */ }
A function, not a live getter: with wakes overlapping, the count moves between reads, and a value read at a moment is honest where a live reference is not.
Deliberately thin — wakes admitted since boot, and nothing else has earned a place yet.
Not the per-wake tick counter phase() stamps against. Two different numbers at two scales.
Availability
Every verb resolves the bound ABI at the moment it is called, never at module scope.
Touching one before load() throws COGNET_ACCESSED_BEFORE_LOAD — do work inside
loop(), not at the top level of main.ts.
wake() and clock() are the exception to wake-scoping: they resolve through the bound
kernel rather than the ambient scope, because a plugin's clock lives in a setInterval
registered at boot and that callback fires outside every wake.