Services
A mind often needs more than its own systems: a language model to talk with, a depth model to see with, speech to hear with. It asks for these as services — by what they do, never by what provides them.
The cognet never names a provider, never bundles one, and never learns which one it got. The agent owns the machine; the cognet only says what it needs. So the same mind runs against a frontier model in the cloud, or one small model on a laptop, without a change.
Four nouns
| Is | Owned by | |
|---|---|---|
| Interface | A typed, versioned contract: each call, what it takes, what it returns, what the data means. chat@1, depth-metric@1. | Published, like a package |
| Use | A cognet's declaration that it needs an interface, under a name of its own. | The cognet |
| Provider | Anything that implements an interface — a local model, a cloud API, a ROS service, another agent. | The machine, or the registry |
| Grant | Which provider fills which use, and what it may cost or reach. | The agent, and its user |
Declaring a use
// cognet.config.ts
export default defineCognet({
mode: { kind: "continuous", hz: 20 },
uses: {
main: { interface: "chat@1", requires: { context: { min: 100_000 } } },
depth: { interface: "depth-metric@1", optional: true },
hear: { interface: "speech-to-text@1", optional: true },
},
suggests: {
depth: "@axon/depth-pro",
},
})
The name — main, depth, hear — is the cognet's own. The user never types it.
requires constrains what the provider's describe() reports. It is what breaks the mind when unmet: a context window too small for what it
renders, an image interface for a mind that sends pictures. There is deliberately no way to
demand a good provider. Quality is the user's trade-off; a mind that could refuse one would be
overruling the person whose machine it runs on.
suggests names a provider the cognet recommends — fetched at axonl prepare, offered to
the agent. A recommendation, never a requirement: the agent may fill the use with anything that
implements the interface.
Using one
Every use a system declares is a handle on its run's services, typed from its interface in every
language.
export default defineSystem({
on: ["depthBeat"],
reads: ["picture"],
writes: ["depth"],
uses: ["depth"],
async run(run) {
if (!run.services.depth.available) return
const [seen] = ecs.query({ components: ["picture"] })
if (!seen) return
const depth = await run.services.depth.estimate(seen.picture)
ecs.set({ id: seen.id, components: { depth: depth } })
},
})
estimate takes a Picture and returns a DepthMap in metres because depth-metric@1 says
so — a typo or a wrong argument is an error in the editor, not at the first tick.
A system names the uses it calls, as it names what it reads. Its services holds those and no
others, so the graph knows which systems depend on depth, and a use no system calls is flagged at
axonl prepare. The config says what each use is; the system says who calls it.
Available, or not
A required use with nothing to fill it fails before the mind starts — at axonl prepare, or
when the agent boots. An optional use may be unfilled, and may come and go while the mind
runs: a provider goes down, a GPU frees up.
available is how a mind asks. Like presence, it is state — as it stood when the run began —
so a system can react to it changing. Calling a use that is not available throws, by name: a
mind asks first, and takes a cheaper path when the answer is no.
describe() says what the granted provider actually is — its features and limits. It is a method
of the interface like any other, so each interface defines what it reports:
run.services.main.describe() // { context: 200_000, modalities: { in: ["text", "image"], out: ["text"] }, slots: 4 }
Three kinds of call
An interface's methods each take one of three shapes:
| Kind | Shape | For |
|---|---|---|
| call | one in, one out | depth, detection, embeddings, transcribing a clip |
| stream | one in, events out | a chat reply, as it is written |
| session | pushes that share state | voice activity, streaming speech, a tracker |
The kind is the call's shape, not the model's architecture: speech synthesis generates sample by sample and is still a call — it takes text, not a conversation.
Handles
Some work is done once and asked about many times. A segmentation model encodes a picture — the costly part — then answers any number of prompts against it. An interface says so by returning a handle: an opaque reference to something the provider keeps.
const encoded = await run.services.segment.encode(picture) // a handle — the provider keeps it
const cup = await run.services.segment.mask(encoded, { point: [120, 80] })
const table = await run.services.segment.mask(encoded, { box: [0, 200, 640, 480] })
await encoded.release()
The picture's embedding never leaves the provider — on a GPU, it stays there. A handle is valid only for the use that made it, and only until it is released, the provider changes, or the run that made it ends. Using one after that is an error, never a quietly different answer.
Off the hot path
A service that takes forty milliseconds cannot sit inside a tick that has fifty — so a system
that calls one is async. The tick carries on; the answer lands on a later tick, about the
picture it was asked about. A mind's reflexes run every tick on what it already believes; what
services add catches up when it can.
Interfaces
ArcLabs publishes a standard library of interfaces, so providers are interchangeable:
| Interface | Does |
|---|---|
chat@1 | a conversation in, a reply out, streamed |
depth-metric@1 | a picture in, depth in metres out |
depth-relative@1 | a picture in, nearer-and-farther out — no unit |
detect@1 | a picture and what to look for in, boxes out |
segment@1 | a picture and prompts in, masks out |
embed@1 | text or a picture in, a vector out |
speech-to-text@1 | sound in, words out |
text-to-speech@1 | words in, sound out |
voice-activity@1 | sound in, a session saying when someone speaks |
Where two kinds of answer mean different things, they are two interfaces — metric and relative
depth are not the same measurement, and a mind that needs one must never be handed the other.
Where they differ only in how good they are — resolution, speed — one interface, and
describe() says.
Anyone can publish an interface; a major version is a promise never to break it.
Held to their meaning
An interface is more than a shape: depth-metric@1 says metres, which way the picture faces,
what a pixel with no answer holds. Every interface ships conformance cases — fixed inputs,
and what any correct provider answers to them — and axon conform runs them against whatever
an agent's grants fill each use with. A strict interface is only strict if something checks.
Your own interface
When nothing standard fits — a model of your own, a research model nobody has an interface for — a cognet declares its own, and suggests the provider that implements it:
// interfaces/scene@1.interface.ts
export interface Scene {
describe(): { maxSurfaces: u32 }
/** Every surface in view, from a picture and its depth. */
encode(picture: Picture, depth: DepthMap): Promise<SceneCode>
}
// cognet.config.ts
uses: { scene: { interface: "./interfaces/scene@1" } },
suggests: { scene: "@cody/scene-encoder" },
Its name is the cognet's (@cody/terry/scene@1), and it is published with it. The provider
still belongs to the agent — the cognet names a contract, never a dependency — but in practice
it will be the one the cognet suggests. That is the price of using something nobody else
provides, and often a fine one.
Interfaces are written in TypeScript, as cognet.config.ts is, and generated into every
language that uses them. See interfaces for how one is
written.
Granting
The cognet says what it needs. The agent decides what fills it — and owns everything that takes: the machine, the credentials, the network, the money.
// axon.config.ts — the agent's, never the cognet's
services: {
main: { provider: "anthropic:claude-sonnet-5-5", limit: { calls: 600, per: "hour" } },
depth: "auto", // whatever this machine has that implements depth-relative@1
features: "./providers/features", // a provider the agent writes
hear: "off", // granted nothing: the use is unavailable
},
| A grant | Means |
|---|---|
a route — "anthropic:claude-sonnet-5-5" | this model, from the agent's inference sources. Not a preference: if nothing supplies it, the use is unfilled |
"./providers/<name>" | a provider the agent writes |
"auto" — or left out | the agent's own provider of the interface if it wrote exactly one; otherwise the best model the machine offers that meets the use's requires |
"off" | nothing — an optional use goes unavailable; a required one stops the agent from starting |
limit | how many calls each second, minute, hour or day — past it, calls are refused with SERVICE_LIMIT |
The machine's models serve the standard interfaces they can: chat@1 from any model the agent's
inference sources supply, depth-relative@1 and segment@1 from local vision models. Any other
interface — a cognet's own — is filled only by a provider the agent writes.
What the cognet never sees: which provider answered, its credentials, where it ran, what it
cost. It sees the interface, available and describe(). Every call is recorded — with the
provider that answered it — in the agent's session, which the agent owns.
Where the work happens: on the agent's side of the boundary, never in the cognet's process. A confined agent's credentials stay with its supervisor; a model on the GPU is loaded once by the machine and shared by every agent that is granted it.
Language models
chat@1 is raw: a conversation in, the model's reply out — streamed or whole. It knows nothing
of how a cognet renders its context or parses a reply. A cognet that wants a grammar — AIR, with
its scripts and blocks — uses the AIR library on top of chat@1, which renders, parses, and
asks again when a reply breaks the grammar. The grammar is the cognet's choice; a provider never
needs to know it.
Learned weights
A model the mind trains as it runs is the mind's own — not a provider's. Its weights are stored through the agent's storage, versioned, and recorded in the session, so a replay knows which weights answered. To run them where they need to — a GPU — the cognet hands them to a provider whose interface runs weights of that kind.
What else it buys
- Tests. A test grants a fake provider. It implements the same interface, so it is held to the same contract as a real one.
- Replay. Every call is recorded with what answered it, so a replay can serve the recorded answers rather than call the model again — a model-heavy session replayed exactly, and free.
- Learning. A learner can be a provider that swaps its weights behind an unchanged interface; the systems using it never notice.
The full surface is services.