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" },
    },
})
FieldMeaning
typeWhich handle shape the cognet's code is written against
in / outAccepted and produced modalities. A single value is shorthand for one
contextMinimum usable window in tokens. Only meaningful for generate
structuredThe reply must be parseable as structured output
parallelA request for more slots, never a threshold
optionalThe brain runs without this. Absent means required
primaryThis 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

EventMeaning
engine:startThe bracket's open half. A consumer showing "thinking" starts here
engine:textThe agent spoke — already interpolated, lang: "md" | "json"
engine:scriptThe model ran code. Reported for the record; the kernel already executed it
engine:failureTerminal. A malformed reply the model failed to correct within its retries
engine:doneExactly 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.