The microkernel and its ports¶
The runtime is a microkernel: a small, closed core that knows how to store, validate, version and compose instances — but knows nothing about any particular Kind. All Kind-specific knowledge is contributed by extensions that plug into the kernel's ports.
This is the mechanism behind the thesis claim that the kernel knows no Kinds.
The kernel as a mediator over five ports¶
Five ports carry the weight, and they are the ones to learn first. Each answers one question:
| Port | Question it answers |
|---|---|
| SourcePort | Where do manifests live? (filesystem, SQLite, Postgres) |
| CachePort | Where are installed dependencies cached? |
| ResolverPort | How are external dependencies fetched? (local:, github:, http(s):) |
| Reader/WriterPort | How is a bundle format detected, scanned and written back? (SKILL.md, SOUL.md, AGENTS.md, YAML) |
| KindPort | What is this Kind's identity, schema and composition role? |
The whole topology in one picture — extensions register Kinds on top, the kernel mediates in the middle, adapters plug in underneath (the two search-plane ports are covered in Search & memory):
flowchart TB
EXT["Extensions<br/>helix · agentskills · soulspec · guardrails · ..."] -->|"kernel.load(ext) registers Kinds"| K
K(["Kernel<br/>(mediator)"])
subgraph five ["The five ports"]
SRC[SourcePort]
CACHE[CachePort]
RES[ResolverPort]
RW["Reader / WriterPort"]
KIND[KindPort]
end
subgraph search ["Search plane"]
EMB[EmbeddingPort]
RSP[RecordSearchProvider]
end
K --> SRC & CACHE & RES & RW & KIND
K --> EMB & RSP
SRC -.- SA["fs · sqlite · postgres · sqlalchemy"]
RES -.- RA["local: · github: · http(s):"]
RSP -.- PA["sqlite-vec · pgvector"]
Because the core only ever talks to these interfaces, you can swap the storage backend, the fetch strategy, or the on-disk format without touching the composition logic — and you can add a Kind without touching the core at all.
Five is where you start, not what there is
These five are the load-bearing ones. They are not the whole surface: DNA
has 61 Protocols, 39 of which are things you are meant to
implement — the conversation purge, the eval target, the contradiction
scribe, the scaffold resolver, the runtime backend, the embedding
provider, and more.
This page names seven of them. For a long time that was the entire public account of DNA's extensibility, which meant the other fifty were seams nobody could find. The full list, each with its contract, the capability it lights up, what the face does if you skip it, and the suite that grades your implementation, is the port catalogue — generated from the source, so a new port cannot ship invisible again.
Extensions register Kinds¶
kernel.load(ext) is the only wiring step. Each extension contributes one or
more KindPorts (and, for custom on-disk formats, a Reader and a Writer).
The kernel validates each registration at boot and fails loud on conflicts —
duplicate (apiVersion, kind) tuples, duplicate aliases, or a Reader/Writer
missing a required method.
Two ways to register a Kind, matching the thesis rule that a Kind is data:
- As data — a record-style Kind with no custom behavior is a
*.kind.yamldescriptor registered withkind_from_descriptor(). No class, no code — and because the descriptor is data, any language can read the same file to learn the Kind's shape. - As code — a Kind that needs a custom bundle format, a typed parse
step, or a composition rule implements a
KindPortclass. See How to add a Kind.
One runtime, any language¶
The ports above make the kernel swappable on the inside. The same microkernel discipline is what makes DNA reachable from the outside in any language — and it is the ports, not a second implementation, that do it.
The runtime is Python (packages/sdk-py). It exposes two
language-neutral faces, each of which is just another adapter over the same
kernel:
| Face | Serve it with | What it is |
|---|---|---|
| REST | dna api serve |
The typed read/write surface, described by an OpenAPI instance (docs/openapi.json) dumped from the live app |
| MCP | dna mcp serve |
The tool face agents speak natively |
Clients are generated from that OpenAPI instance, not hand-written:
dna-client ships for TypeScript
and Python, and any language with an
HTTP client can reach the same routes. Because the types come from the spec,
a client cannot silently drift from the runtime — regeneration is a diff, and
CI fails when the committed types are stale.
Why not a second kernel?
DNA used to ship one: packages/sdk-ts, a full TypeScript twin of the
Python kernel kept 1:1 by hand, with parity policed by shared
fixtures, descriptor hash checks and a kind-registry manifest. The
promise — DNA runs anywhere — was right; the mechanism made every
kernel change a two-language change, and the parity harness could only
ever detect drift after the fact.
The twin is frozen at the tag
sdk-ts-final
and no longer maintained. Portability now costs one implementation
instead of two, and the generated clients make drift structurally
impossible rather than merely tested.
The port contract¶
A source adapter is only production-ready when it satisfies the port contract — a suite that runs the same battery over every adapter and refuses to let a claimed capability go unimplemented. The full contract, its capability protocols, and the conformance kit for authoring a new adapter are in How to write a source adapter.
One rule from that contract generalizes to every port, and it is the thing
worth carrying away from this page: a store that cannot answer must refuse,
not approximate. An adapter that keeps no reference graph does not return an
empty list of references — [] reads as nothing points at this instance, a
claim only a store that actually records edges may make, so the face answers
unsupported instead. Capabilities are declared up front precisely so a face
can tell "I found nothing" apart from "I cannot know", and the catalogue says
which answer each port produces when you leave it unimplemented.
The EmitterPort — materialize per runtime¶
The five ports above are the kernel's inward contracts: they answer how the core stores, resolves and composes instances. There is one more first-class, documented DNA port that sits one layer out — the EmitterPort. Where the kernel composes a neutral agent, an emitter materializes it into the native artifact a specific runtime consumes (the de-para): author once in DNA, emit per runtime, swap runtimes without a rewrite.
| Port | Question it answers |
|---|---|
| EmitterPort | How is a composed agent materialized into a target runtime's native artifact? (agent-framework, bedrock, vertex, openai-agents, …) |
It is a genuine port — a documented contract, not a hardcoded switch — so a new
target is a class plus one register_emitter(...) call and the emit core never
changes. The contract has two surfaces (build_emit_context(mi, agent) composes
and projects the neutral view once; emit(ctx) materializes it per target) and
one central invariant: the composed instruction in the emitted artifact is
byte-equal to build_prompt — the emit carries the composition verbatim.
That invariant is inheritable (extract_instructions recovers it from any
target's own artifact, and one generic test runs the check over every
registered target).
Two flavors satisfy the same port: config-declarative (map onto a runtime's
published YAML/JSON schema) and scaffold-code (fill a curated
{framework × case} template for a code-first runtime). The full step-by-step is
in How to write an emitter.
Where to go next¶
- Kinds — identity and composition — what a
KindPortcontributes and how composition works. - How to add a Kind — register your own onto these ports.
- How to write a source adapter — the SourcePort contract in full.
- The port catalogue — all 61 ports, grouped by what you are trying to change.