kernel.engine
Inference, by role. A cognet names what an engine is for and never which model fills it — that is decided at boot against whatever providers the user declared.
type KernelEngines = {
/** The handle for a declared role. THROWS for an unbound one. */
(role: string): KernelEngine
/** Is this role filled? The entire degradation contract. */
has(role: string): boolean
}
type KernelEngine = GenerateEngine | TransformEngine | StreamEngine
/** What every handle reports about what it actually got. */
type EngineFacts = {
/** Usable context window. Undefined when the source does not report one. */
readonly context: number | undefined
readonly modalities: { in: readonly Modality[]; out: readonly Modality[] }
/** Concurrent calls this role may run. At least 1, never 0. */
readonly slots: number
}
type GenerateEngine = EngineFacts & {
readonly type: "generate"
stream(req: CognetEngineCall): AsyncGenerator<AxonEngineEvent>
request(req: CognetEngineCall): Promise<AxonEngineResponse>
}
type TransformEngine = EngineFacts & {
readonly type: "transform"
transform(input: unknown, opts?: TransformOptions): Promise<unknown>
}
type StreamEngine = EngineFacts & {
readonly type: "stream"
/** Begin one sequence. Every push into it shares the model's hidden state. */
open(): EngineSession
}
type Modality = "text" | "image" | "audio" | "video" | "vector" | "score" | "depth"
Callable with has hung off it, so the common case reads as one verb and the degradation
check reads as a question.
Declaring roles
// cognet.config.ts
export default defineCognet({
engines: {
main: { type: "generate", in: "text", out: "text", context: 100_000 },
percept: { type: "generate", in: ["text", "image"], out: "text", parallel: true, optional: true },
vad: { type: "stream", in: "audio", out: "score" },
},
})
| Field | Meaning |
|---|---|
type | Which handle shape the cognet's code is written against |
in / out | Accepted and produced modalities. A single value is shorthand for one |
context | Minimum usable window in tokens. Only meaningful for generate |
structured | The reply must be parseable as structured output |
parallel | A request for more slots, never a threshold |
optional | The brain runs without this. Absent means required |
primary | This role is what the user's model picker edits |
The key is the cognet's private vocabulary and the user never types it — a user who had to name a brain's roles would be wiring one specific brain into their setup.
Constraints are structural only. Every field is something that breaks the brain when unmet, never something that merely makes it worse: a context window too small means the render does not fit; a text-only model handed an image cannot answer at all. There is deliberately no way to demand a good model. Quality is the user's tradeoff, and a cognet that could refuse one would be overruling the person whose machine it runs on.
Required, optional, and the degradation path
A required role with nothing to fill it fails at axon prepare — never at the first tick.
const summary = kernel.engine.has("compress")
? await kernel.engine("compress").request({ messages })
: truncate(messages) // the cheaper route
has() is the whole degradation story. It is what lets one brain run against a frontier
account and against a single local model without knowing which it got.
Calling an unbound role throws, deliberately. A required role that could not be filled already stopped the boot, so reaching there means the cognet called an optional engine without asking first — a cognet bug, and a null handle would only move the crash one frame later with less to say about it.
parallel is a request, not a threshold
const engine = kernel.engine("percept")
const batch = chunk(frames, engine.slots) // branch on what you got
The count a cognet would name is a fact about a machine it cannot see, and asserting one turns "runs slowly" into "refuses to install" for exactly the users who most need it to degrade. One slot is N sequential calls: slower, still correct, never zero.
context, modalities and slots report what the role actually got, which is how a
brain that declared a floor decides how hard to push against it.
generate — autoregressive over messages
for await (const event of kernel.engine("main").stream({ messages, protocol: "classic" })) {
if (event.type === "engine:text") { /* the agent spoke */ }
if (event.type === "engine:script") { /* the model ran code */ }
if (event.type === "engine:done") { /* signals */ }
}
type CognetEngineCall = {
messages: AxonEngineMessage[]
model?: string
maxTokens?: number
temperature?: number
/** The output grammar to parse the reply with. Must match what you rendered. */
protocol?: "classic" | "raw"
/** Re-render from the session for a retry — see below. */
rerender?(): Promise<AxonEngineMessage[]>
}
This is AxonEngineCall minus the fields the kernel owns. signal is the wake's
cancellation, applied unconditionally — a cognet never passes one and so can never omit
one. output and retries are the caller's structured-output contract; a cognet has no
business knowing a shape was demanded. role is supplied by the handle you called.
protocol matters: a cognet rendering one grammar while the kernel parses another would
silently discard every block the model emitted. The caller that rendered the context owns
this.
generate is defined by its call shape, not by whether the model samples
autoregressively. Text-to-speech is a transform even though it generates token by token,
because it takes a string rather than a timeline and nothing about the grammar applies.
The event stream
| Event | Meaning |
|---|---|
engine:start | The bracket's open half. A consumer showing "thinking" starts here |
engine:text | The agent spoke — already interpolated, lang: "md" | "json" |
engine:script | The model ran code. Reported for the record; the kernel already executed it |
engine:failure | Terminal. A malformed reply the model failed to correct within its retries |
engine:done | Exactly once per successful call, with the response and billing meta |
Raw engine:* wire events. The kernel does not pre-label them as the cognet's output:
when a text block arrives, it is the cognet that decides whether to output() it; when a
typescript block arrives, it is the cognet that decides whether to run() it. That
decision belongs to the cognet alone.
engine:text chunks are chunks of rendered output, so they no longer correspond 1:1
to model tokens — one delta can emit nothing (buffered mid-interpolation) or a great deal
(a large interpolated value).
The done signals
| { type: "engine:done"
response: AxonEngineResponse
spoke: boolean // produced user-facing output this call
acted: boolean // emitted a script it has not yet seen the result of
yielded: boolean } // emitted <done/> — its own claim it is handing control back
These are signals, not decisions — reductions over what this call produced, from which a loop derives its own stop condition. The kernel never acts on them and deliberately does not decide whether a turn is over: that is the one thing a loop is for.
yielded is here under protest. Whether a turn is over is a semantic question — "I see
the issue" and "the fix is deployed" are structurally identical, same block and no script —
so no reduction over what a response did can separate a progress report from a final
answer. Deriving it structurally ends a long run the first time the model narrates between
actions. So the model is asked, which works consistently enough, and it stays a signal a
loop weighs against the structural facts beside it.
request — the one-shot form
const { text, stopReason, meta } = await kernel.engine("classify").request({ messages })
Same response shape the stream's done event carries. stopReason is "end",
"length" (truncated at maxTokens — trailing blocks are incomplete) or "abort".
meta.tokens and meta.cost are absent when the provider does not report them, never
fabricated as zero. Local inference has no price.
transform — one shot in, one shot out
const transcript = await kernel.engine("asr").transform(pcm)
const vector = await kernel.engine("embed").transform("the query")
await kernel.engine("image").transform(prompt, {
onProgress: ({ fraction, message }) => { /* fraction is 0..1, or absent */ },
})
ASR, embeddings, classifiers, depth, TTS. The kernel does not interpret either side. A depth map is a shaped array, a transcript is text with timings, an embedding is a vector; what the bytes mean is the cognet's business. The one thing this contract guarantees is that the call happened and the result came back.
onProgress exists because "one shot" is about shape, not duration: an image
generation is thirty seconds of denoising steps, and a bare promise makes that
indistinguishable from a hang. fraction is present only where the model knows its own
total, so a caller renders a spinner rather than a bar that lies. Absent entirely for the
many transforms that finish in milliseconds.
stream — a stateful sequential feed
const session = kernel.engine("vad").open()
for (const frame of frames) {
const speech = await session.push(frame) // order is significant
}
session.reset() // new utterance, same session
session.close() // release the sequence; the engine stays loaded
type EngineSession = {
push(input: unknown): Promise<unknown>
reset(): void
close(): void
}
Distinct from transform because the model carries hidden state across calls. Silero's
LSTM is what lets it tell a pause mid-sentence from silence, and feeding frames out of
order or through two sessions destroys exactly that. So a caller opens one session and
pushes into it rather than making N independent calls.
open() is where the expensive part happens once — building the execution graph. reset()
forgets the sequence while keeping the session, because every stateful model has some
version of "this is a new utterance" and reopening would rebuild the graph that a session
exists to amortise.
The session is the cognet's to hold. It is resident memory of precisely the kind a brain keeps.
What the kernel still owns
Auth, metering, quotas, the retry budget, and the grammar the reply is parsed with. What it no longer decides is which model — and the cognet still never learns.