---
url: 'https://adk.nht.io/what-adk-is.md'
description: >-
  The library's ethos — what ADK owns, what it deliberately doesn't, and the
  design choices that follow from that line.
---

# What ADK is, and what it isn't

## LLM summary — What ADK is

* ADK is a **kit**, not a platform. It owns one execution shape (the turn) and the seams around it (tools, middleware, events, primitives). It does not own infrastructure choices (model, storage, transport, runtime, deployment).
* Five principles every decision is measured against: bring your own everything; fail fast, fail loudly; immutability by default; cross-realm safety (duck-typed guards + constructor-name fallbacks); token-aware from the start.
* Three lines that capture the ethos:
  * "ADK doesn't let you be vague by accident." — the contract surfaces are explicit. Required callbacks must be supplied; omission is a config-validation failure, not a silent no-op.
  * "If you want a no-op, you have to say so out loud." — the library refuses to invent permissive defaults. A `noop` storage callback is fine; the ADK will not pick one for you. `TurnRunnerConfig` requires twenty-five storage callbacks (seven fetch + eighteen persistence) at construction.
  * "Movement is a design requirement, not a future apology." — every seam exists because you must be able to swap the thing behind it without restructuring the loop.
* What ADK is opinionated about: turn lifecycle, validation eagerness, immutability of constructed objects, schema-owned tool definitions, the trust-tier rendering contract, the handle pattern for large artifacts, the functional/observability event split, the rule that errors emit (not throw) for non-fatal failures, `run()` returns `Promise<void>` permanently, dual-budget thinking (runtime memory and context window).
* What ADK is deliberately unopinionated about: model provider, prompt format, storage technology, retrieval strategy, memory schema, runtime (browser / worker / node / desktop), observability stack, deployment target, retry policy, iteration bounds, conversation-loop management, multi-agent coordination.
* Pages downstream (`/the-loop/*`, `/assembly/*`) are the contract-level reads of the same ethos.

This page is about the library itself: what `@nhtio/adk` is for, what it
refuses to do, and the small number of opinions it does hold and why.

ADK draws a single, deliberate line. On one side: the execution shape of a
turn — input pipeline, dispatch loop, output pipeline, events, validated
primitives. The ADK owns that shape and is strict about it. On the
other side: every infrastructure choice a real application makes — which
model you call, where state lives, how prompts are built, how retrieval
works, which runtime you ship in. The ADK owns none of that and will
not pretend to.

Everything below is a closer read of that line. If the vocabulary —
*turn*, *dispatch*, *iteration*, *tool*, *context*, *middleware* —
hasn't landed yet, [How agents work](./how-agents-work) is the
plain-English orientation; come back here once those words feel
concrete.

## What ADK is

**A kit, not a platform.** ADK owns one shape — *the turn* — and the small set of seams
that make a turn deterministic: input middleware, an executor for the model
call, output middleware, validated primitives that travel through them, two
event buses, and a tool registry with one collision policy. That's it.
[The Loop](./the-loop/) is the page-by-page tour of the shape.

**A contract surface.** Every seam has a typed signature, every primitive
validates at construction, and every error has a stable code. If the ADK
runs, the inputs were valid and the outputs are well-shaped — the parts of
agent systems that fall over in production because they were "mostly valid"
do not exist in this codebase.

**A movement guarantee.** The seams exist so you can swap what's behind them.
Today's hosted-API executor is tomorrow's different-hosted-API executor is
next quarter's local-model executor, and none of them require restructuring
the loop. Same for storage, retrieval, memory, and tool catalogues.

## Claims with a working proof

"Bring your own everything." "Runs anywhere TypeScript runs." Every
agent framework prints those words. Almost none of them survive
contact with a runtime that wasn't on the author's laptop. We got
tired of the bluff, so we called it on ourselves.

::: tip Try "Ask ADK"
See the **Ask ADK** button in the top-right? Click it. Right now.

What just woke up is a language model running *in your browser*. Not
a proxy. Not a server call wearing a costume. A real model, on your
hardware, on a tab you can close, given tools by ADK so it can
actually answer questions about this site. Pull your network cable.
It still works. We'll wait.

We didn't ship that because it was easy. We shipped it because if
ADK couldn't hold its contracts together with the model swapped, the
storage swapped, the runtime swapped, and the network gone — then
every word on this page would be marketing fiction and you'd be
right to close the tab.

It held. Same [`TurnRunner`](https://adk.nht.io/api/@nhtio/adk/turn_runner/classes/TurnRunner). Same tools. Same events. The
[Showcase](./showcase/ask-adk) walks through how. Read
it when you're done being skeptical.
:::

## What ADK isn't

**Not a model client.** ADK never calls a provider. The [`DispatchExecutorFn`](https://adk.nht.io/api/@nhtio/adk/dispatch_runner/type-aliases/DispatchExecutorFn) you
write is the only place a model is called, and you write it. Bundled
batteries are reference implementations of that seam — useful defaults,
not the load-bearing path.

**Not a database.** ADK never persists anything. Storage callbacks (`fetch*`,
`store*`, `mutate*`, `delete*`) are required because state has to live
somewhere; *where* it lives is entirely your problem. In-memory map, SQLite,
Postgres, IndexedDB, no-op for tests — ADK does not care.

**Not a prompt template engine.** The [`TurnContext.systemPrompt`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/TurnContext#property-systemprompt) and standing instructions
are [`Tokenizable`](https://adk.nht.io/api/@nhtio/adk/common/classes/Tokenizable) text. The ADK doesn't run a templating language, doesn't
inject variables, doesn't manage versions. If you want templates, bring a
template engine. Most developers don't need one.

**Not an orchestrator.** ADK does not retry, queue, schedule, fan out, or
manage backpressure. One [`TurnRunner.run`](https://adk.nht.io/api/@nhtio/adk/turn_runner/classes/TurnRunner#run) call is one turn. Higher-level orchestration
— retries, multi-turn flows, conversation persistence — lives in your code.

**Not a hosted runtime.** There is no ADK server, no ADK cloud, no ADK
control plane. The library is a TypeScript package; the runtime is wherever
you run it (browser, worker, Node, Electron, Deno, Bun, CLI). No hidden
infrastructure.

## Where ADK is opinionated

The opinions are deliberate and few. They exist because the parts of agent
systems that *don't* have these opinions get the same bugs over and over.

### Validation is eager, not eventual

The runner refuses to construct with an incomplete config. Tools refuse to
construct with an invalid schema. Primitives refuse to construct with missing
fields. There is no "we'll figure it out at call time" — if the inputs were
wrong, you find out immediately, with a stable `E_*` exception code, at the
seam where the bad input arrived.

::: tip ADK doesn't let you be vague by accident
You will not get a partial ADK, a partial tool, or a partial primitive.
Construction either succeeds with all required pieces present, or it throws.
There is no third state.
:::

### Required callbacks are required

If [`TurnRunnerConfig.fetchMessagesCallback`](https://adk.nht.io/api/@nhtio/adk/turn_runner/interfaces/TurnRunnerConfig#property-fetchmessagescallback) is part of the contract, you supply it. The
ADK will not synthesise an empty array on your behalf, will not log a
warning and continue, will not pick "in-memory" by default. The absence of
a callback is a config-validation failure.

[`TurnRunnerConfig`](https://adk.nht.io/api/@nhtio/adk/turn_runner/interfaces/TurnRunnerConfig) specifically demands **twenty-five storage callbacks**
at construction time: seven retrieval callbacks
([`TurnRunnerConfig.fetchMemoriesCallback`](https://adk.nht.io/api/@nhtio/adk/turn_runner/interfaces/TurnRunnerConfig#property-fetchmemoriescallback), [`TurnRunnerConfig.fetchMessagesCallback`](https://adk.nht.io/api/@nhtio/adk/turn_runner/interfaces/TurnRunnerConfig#property-fetchmessagescallback),
[`TurnRunnerConfig.fetchThoughtsCallback`](https://adk.nht.io/api/@nhtio/adk/turn_runner/interfaces/TurnRunnerConfig#property-fetchthoughtscallback), [`TurnRunnerConfig.fetchToolCallsCallback`](https://adk.nht.io/api/@nhtio/adk/turn_runner/interfaces/TurnRunnerConfig#property-fetchtoolcallscallback),
[`TurnRunnerConfig.fetchRetrievablesCallback`](https://adk.nht.io/api/@nhtio/adk/turn_runner/interfaces/TurnRunnerConfig#property-fetchretrievablescallback), [`TurnRunnerConfig.fetchToolsCallback`](https://adk.nht.io/api/@nhtio/adk/turn_runner/interfaces/TurnRunnerConfig#property-fetchtoolscallback),
[`TurnRunnerConfig.refreshStandingInstructionsCallback`](https://adk.nht.io/api/@nhtio/adk/turn_runner/interfaces/TurnRunnerConfig#property-refreshstandinginstructionscallback)) and eighteen persistence
callbacks (store / mutate / delete for each of memories, messages,
thoughts, tool calls, retrievables, and standing instructions). There
is no construction path that omits persistence — every ADK must
have a storage layer wired up at the boundary, even if that layer is
an in-memory stub for testing.

::: warning If you want a no-op, you have to say so out loud
Passing `async () => []` is fine. Passing nothing is not. The ADK refuses
to guess which of those you meant — because half the time the guess is wrong,
and you don't notice until production.
:::

### Immutability by default

Constructed objects expose read-only properties. Mutation happens only
through explicit controlled APIs — [`Registry.set`](https://adk.nht.io/api/@nhtio/adk/common/classes/Registry#set), [`Tokenizable.set`](https://adk.nht.io/api/@nhtio/adk/common/classes/Tokenizable#property-set),
`ctx.storeMemory(m)` (see [`TurnContext.storeMemory`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/TurnContext#property-storememory)), `ctx.mutateMessage(id, patch)` (see [`TurnContext.mutateMessage`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/TurnContext#property-mutatemessage)). There is no
"sometimes the object is frozen, sometimes it isn't." Read-only getters
return the actual mutable Set instances (so `ctx.turnMemories.add(m)`
works) but you cannot assign `ctx.turnMemories = new Set()` (see [`TurnContext.turnMemories`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/TurnContext#property-turnmemories)) — structural
replacement is forbidden, in-place mutation through the documented APIs
is the path.

### Cross-realm safety is structural

Every class identity guard uses the [`isInstanceOf`](https://adk.nht.io/api/@nhtio/adk/guards/functions/isInstanceOf) helper (which runs
`instanceof`, then `Symbol.hasInstance`, then `constructor.name`) rather
than a bare `instanceof`. This is load-bearing: a consumer's bundle can
end up with two copies of the ADK in memory (one in `node_modules`,
one bundled by a downstream library), and bare `instanceof` will return
`false` for instances created against the "other" copy. The ADK
treats that as a foreseeable runtime, not an edge case.

### `run()` returns `Promise<void>` — intentionally and permanently

This is not a gap to be filled later. All meaningful output surfaces via
events: `message`, `thought`, `toolCall`, `error`, `turnStart`, `turnEnd`.
Awaiting `run()` only signals that the pipeline finished; it carries no
data. Streaming responses arrive incrementally mid-turn, tool calls are
dispatched and settled before the turn ends, and callers may want to act
on output before the turn completes. Returning data from `run()` would
force callers to wait for completion before they could act, which is the
wrong model for an agent loop.

### Two budgets, always

Every artifact and every primitive in the library is designed against two
simultaneous constraints: runtime memory and LLM context window. Both are
finite; violating either produces a failure — an OOM crash or a truncated
model call. [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) holds a [`SpoolReader`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/interfaces/SpoolReader), not bytes;
[`SpooledMarkdownArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledMarkdownArtifact) caches only structural metadata, never the
document body; [`SpooledArtifact.cat`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#cat) fetches only the requested range.
Token-aware design isn't an afterthought, it's the reason [`Tokenizable`](https://adk.nht.io/api/@nhtio/adk/common/classes/Tokenizable)
exists as the string primitive everywhere prompts, instructions, and
memory content appear.

### No safety net — primitives, not policies

[`DispatchRunner`](https://adk.nht.io/api/@nhtio/adk/dispatch_runner/classes/DispatchRunner) has no `maxIterations`, no `maxToolCallChecksumRepeats`,
no `timeout`. The primitives — `ctx.iteration` ([`DispatchContext.iteration`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#property-iteration)), `ctx.toolCallCount(checksum)` ([`DispatchContext.toolCallCount`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#toolcallcount)),
`ctx.ack()` ([`DispatchContext.ack`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#ack)), `ctx.nack()` ([`DispatchContext.nack`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#nack)), `ctx.abortSignal` ([`DispatchContext.abortSignal`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#property-abortsignal)) — are sufficient for any
termination bound you need. Some developers want an iteration cap; some
want both iteration and checksum bounds; some want a wall-clock timeout
via an external `AbortController`. The runner provides the primitives and
stays out of the policy decision.

::: danger There is no default cap. None.
If your executor never calls [`DispatchContext.ack`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#ack), the loop runs until the abort
signal fires or the process is killed. This is intentional — the ADK
has no opinion about how long your agent should think — but it means
*you* must write the bound. [LLM Dispatch](./the-loop/llm-dispatch)
documents the patterns.
:::

### Tools are schema-owned

Every tool has one `@nhtio/validation` object schema. That schema validates
arguments at call time **and** produces the description the model sees in
its tool definition. There is no separate "JSON schema for the model" and
"runtime validator for the handler" — those are the same artifact, by
construction. The model cannot be told one contract while the handler
enforces another.

### Trust is structural

Content rendered into the prompt is wrapped in distinct envelopes per trust
tier (developer policy, trusted tool output, untrusted text, retrieved
context), and the closing tags carry nonces bound to the producing
primitive's id. This is not decoration — it is the load-bearing defence
against prompt-injection attacks. [Trust Tiers](./the-loop/trust-tiers)
covers the mechanism; its per-tier research sub-pages (e.g.
[envelopes research](./the-loop/trust-tiers/envelopes/research),
[media research](./the-loop/trust-tiers/media/research)) carry the threat
model and citations.

### Errors emit, they do not throw (mostly)

Fatal errors (invalid config, invalid context, half-built primitives) throw
synchronously at the seam where they arrived. Non-fatal errors (middleware
failures, executor failures, tool handler failures) emit on the observability
bus as `error` events and let the turn settle. The split is intentional:
programmer mistakes are loud; runtime failures are observable. You cannot
swallow a non-fatal error by forgetting to wire `runner.observe('error')` —
the ADK still settles cleanly, and your telemetry just doesn't see it.

### The handle pattern

Large tool results don't get inlined into the next prompt. They become
[`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) handles, and the model gets ephemeral `artifact_*` tools
to query them by range. This keeps context windows survivable and memory
bounded. [Artifacts](./the-loop/artifacts) is the contract; [Budgets](./the-loop/budgets)
is the why.

## Where ADK is deliberately unopinionated

Everything not on the list above. To make the line crisp:

| Decision | Owned by |
| --- | --- |
| Which model provider you call | You. The [`DispatchExecutorFn`](https://adk.nht.io/api/@nhtio/adk/dispatch_runner/type-aliases/DispatchExecutorFn) is yours. |
| How you format the request to that provider | You (or the LLM battery you picked). |
| Where messages, memories, retrievables live | You. The storage callbacks are yours. |
| How you retrieve relevant documents | You. Retrieval is an input-middleware concern. |
| How memory is scored, ranked, or forgotten | You. ADK defines the [`Memory`](https://adk.nht.io/api/@nhtio/adk/common/classes/Memory) primitive, not its lifecycle. |
| How tool calls are dispatched to handlers | You. The ADK emits `toolCall` events; middleware owns dispatch. |
| How many iterations a dispatch may run | You. There is no built-in cap. |
| Whether to retry on failure | You. ADK does not retry. |
| When and how often to start a turn | You. ADK has no conversation-loop manager. |
| How multiple agents coordinate | You. ADK has no multi-agent orchestrator. |
| How prompts are templated | You. ADK is not a template engine. |
| How you observe the loop | You. The observability bus is plumbing; you bring Sentry / OpenTelemetry / pino / nothing. |
| Which runtime you run in | You. Browser, worker, Node, Electron, Deno, Bun, CLI — same contracts. |
| How you deploy | You. ADK has no deployment story because there is nothing to deploy. |

::: tip "Bring your own everything" is not a slogan — it's the API shape
The seams are what you compose. The batteries are what you import when a
default would save you time. Everything else is yours and stays yours.
:::

## What's in the box, what isn't

The exact in-scope / out-of-scope line, from the package README:

**In scope.** Turn execution engine ([`TurnRunner`](https://adk.nht.io/api/@nhtio/adk/turn_runner/classes/TurnRunner)) with paired
input/output middleware pipelines around a user-supplied executor;
validated immutable context threading ([`TurnContext`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/TurnContext)); single LLM
dispatch context ([`DispatchContext`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext)) with [`DispatchContext.ack`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#ack) / [`DispatchContext.nack`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#nack) lifecycle;
single-use dispatch orchestrator ([`DispatchRunner`](https://adk.nht.io/api/@nhtio/adk/dispatch_runner/classes/DispatchRunner)); executor
helpers ([`DispatchExecutorHelpers`](https://adk.nht.io/api/@nhtio/adk/dispatch_runner/interfaces/DispatchExecutorHelpers)) for per-id streaming state; memory
modelling ([`Memory`](https://adk.nht.io/api/@nhtio/adk/common/classes/Memory) shape and validation, not storage); multi-backend
token counting ([`Tokenizable`](https://adk.nht.io/api/@nhtio/adk/common/classes/Tokenizable)); cross-middleware key-value scratchpad
([`Registry`](https://adk.nht.io/api/@nhtio/adk/common/classes/Registry)); structured machine-readable exceptions with a [`ValidationException.fatal`](https://adk.nht.io/api/@nhtio/adk/exceptions/classes/ValidationException#property-fatal)
classification; event streaming for message chunks, reasoning traces,
and tool call lifecycle; Chat Completions-compatible message contracts;
schema-first tool definitions and registry ([`Tool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool), [`ToolRegistry`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry));
lazy, line-oriented artifact types ([`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact),
[`SpooledJsonArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledJsonArtifact), [`SpooledMarkdownArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledMarkdownArtifact)).

**Out of scope.** LLM API calls (no provider SDK in the dependency tree).
Storage (no opinion on where memories or messages persist). Tool
execution dispatch (no built-in function-call loop or result routing —
middleware owns dispatch). Prompt templating. Conversation-loop
management (the caller decides when and how often to invoke a turn).
Multi-agent coordination.

::: info Batteries included — but only the ones you ask for
The `@nhtio/adk/batteries` subpath ships pre-constructed compute tools
(math, datetime, encoding, parsing, statistics, color, geo, text
analysis, etc.) and storage helpers. They are **not** re-exported from
the root entry, so a consumer who never imports them pays nothing in
their bundle. Their third-party requirements (`mathjs`, `chrono-node`,
`papaparse`, `flydrive`, etc.) are declared as **optional peer
dependencies** — `pnpm install @nhtio/adk` does not pull them in. The
BYO-everything principle holds: a consumer who never imports the
`parsing` battery never installs `papaparse`.
:::

## How to read the rest of the docs

* [Quickstart](./quickstart) — install the package and wire the smallest
  possible turn. Get the muscle memory before you read the contracts.
* [The Loop](./the-loop/) — the contract surface. One page per seam.
* [Assembly](./assembly/) — how to wire your executor, storage, tools, retrieval,
  and memory into a working agent.
* [Trust Tiers](./the-loop/trust-tiers) — the deepest of the opinions above,
  with per-tier research sub-pages carrying the threat models and citations to
  the security literature that informs them.
* [API reference](./api/) — Typedoc-generated, never drifts.
* [Glossary](./glossary) — every term, in one place, with links into the
  pages that own the contract.

::: info A reasonable test
If you can describe what the ADK owns, what it refuses to own, and
which of your existing systems will fill each unowned seam — you are ready
for [Quickstart](./quickstart). If not, [How agents work](./how-agents-work)
is the page that orients first.
:::
