Agent Development Kit · Start
The next hour, recipe by recipe
You have run an agent in this page. These six recipes move it onto your machine, clone the sample that already does the hard part, give the agent the web, put it behind a browser UI and a terminal UI, and name the errors you will actually hit. Every shell command here is real and every quoted error is the string the package throws.
Recipe 1
From this page to a project on your machine
Five commands. The ADK needs Node 22 or newer, and the code below uses top-level
await, so the project is ESM.
mkdir math-agent
cd math-agent
npm init -y
npm pkg set type=module
npm install @animahealth/adk zod
npm install -D tsx typescript @types/node
zod is the ADK's one required peer, so your package manager installs it anyway —
naming it makes your own import { z } from 'zod' honest. You do not need
the OpenAI SDK to run: @animahealth/adk/openai ships it inside that
subpath's bundle, which is why the cells on this site reach api.openai.com with
nothing else installed. A TypeScript project does need it, because that subpath's type
declarations reference the SDK's types — so either npm install -D openai or set
skipLibCheck: true (the shipped sample does). The one path that loads the real
SDK at runtime is the deprecated core-entry re-export,
import { openai } from '@animahealth/adk'; the subpath is the supported import.
Then the key. It is read from the environment at the first model call, not at import — the adapter builds its endpoint list right before it needs one.
export OPENAI_API_KEY=sk-...
Now the program. This is the body of agent.ts, and it is the same agent this
site's quickstart ran: one tool with a Zod schema, one agent that carries it. Press Run — the
cell builds it here too.
import { adk } from '@animahealth/adk'
import { openai } from '@animahealth/adk/openai'
import { z } from 'zod'
const app = adk()
const calculator = app.tool({
name: 'calculate',
description: 'Evaluate a mathematical expression',
schema: z.object({ expression: z.string() }),
execute: (ctx) => {
const sanitized = ctx.args.expression.replace(/[^\d\s+*/().-]/g, '')
return { result: Function(`"use strict"; return (${sanitized})`)() }
},
})
const assistant = app.agent({
name: 'math_assistant',
model: openai('gpt-5.6-luna'),
context: [app.context.system('Use the calculator for arithmetic.'), app.context.history()],
tools: [calculator],
})
assistant.name
A file needs two more lines than a cell does — the cell prints its last expression, your program has to say so:
const run = await app.run(assistant, 'What is 731 * 268, minus 17?')
console.log(run.output.text)
npx tsx agent.ts
That is the whole loop: no server, no config file, no framework directory. The last thing
worth adding before you build on it is a test that needs no key —
npm install -D vitest, then npx vitest run, with the model's turns
scripted while your tools really execute. The testing chapter has the
whole kit.
Recipe 2
Clone the Bookings sample and run it
Bookings is a slot-booking assistant that offers an appointment and then stops:
booking is a yielding tool, so the session becomes a row in a SQLite file and a later command
supplies the approval. It lives under sample/ and depends on the ADK beside it
("@animahealth/adk": "file:.."), so the repository root is built once first:
git clone https://github.com/mycontinuum-com/adk.git
cd adk
pnpm install && pnpm run build
cd sample
npm install
npx tsx src/cli.ts ask "I need physio on Tuesday afternoon, for Alex Doe"
ask calls a model, so export OPENAI_API_KEY first — then walk the
three sources, the resume commands, and the suite that proves the pause with no key in
the Bookings sample.
Recipe 3
Give the agent the web
Three web tools ship in the core entry as factories on app.tools. Each returns an
ordinary tool, so it goes in the same tools: array your own tools do. The model
sees them as web_search, fetch_page and
take_screenshot.
const researcher = app.agent({
name: 'researcher',
model: openai('gpt-5.6-luna'),
context: [
app.context.system('Search before you answer. Fetch a page when the snippet is not enough.'),
app.context.history(),
],
tools: [
app.tools.webSearch({ numResults: 5, searchType: 'web', country: 'GB' }),
app.tools.fetchPage({ render: true }),
],
})
Search goes through Serper, and its key comes from SERPER_API_KEY. The provider
is constructed when you build the tool, so a missing key throws while the agent is being
assembled — before any model call, which is the good time to find out. Pass one explicitly
instead with app.tools.webSearch({ provider: new SerperProvider(key) }),
importing SerperProvider from @animahealth/adk/web.
fetch_page takes one URL or an array, and returns web pages as markdown, PDFs as
documents and images as images — the media rides back with the tool result, so a vision model
can read it. Its markdown conversion is done by three optional peers you have to install:
npm install jsdom @mozilla/readability turndown. render: true and
take_screenshot additionally need playwright plus
npx playwright install, and screenshots need sharp. Install nothing
and the failure is quiet — see recipe 6. The
web tools chapter covers the configs; for tools someone else wrote,
the ADK speaks MCP.
Recipe 4
Stream it into a browser
app.handler.agui({ agent }) returns a function from one request to an async
iterable of AG-UI events — the protocol the off-the-shelf
chat frontends already speak. Write each event down an SSE response and a browser has a
streaming UI without you inventing a message format. Install the protocol's types first:
npm install @ag-ui/core (an optional peer, pinned to one exact version in the
package's peerDependencies).
import { createServer } from 'node:http'
// One handler, built once and called per request. The app's store and hooks come with it.
const stream = app.handler.agui({ agent: assistant })
createServer(async (request, response) => {
const url = new URL(request.url ?? '/', 'http://localhost')
response.writeHead(200, {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
connection: 'keep-alive',
})
for await (const event of stream({
// No session id starts a new session; passing one back continues that conversation.
sessionId: url.searchParams.get('session') ?? undefined,
input: { message: url.searchParams.get('q') ?? '' },
})) {
response.write(`data: ${JSON.stringify(event)}\n\n`)
}
response.end()
}).listen(3000)
The browser end is three lines, because EventSource is doing the work:
const events = new EventSource(`/agent?q=${encodeURIComponent(question)}`)
events.onmessage = (message) => render(JSON.parse(message.data))
The event vocabulary is catalogued in serving it; the part that matters
here is the pause — a run that stops at a yielding tool sends a CUSTOM event
named RUN_INTERRUPTED, carrying reason: 'tool_yield' with the tool's
name and arguments (or reason: 'input_required' when the agent is waiting for the
next message), and then emits RUN_FINISHED twice, only the second of which
carries result.
Two siblings, same config object. app.handler.rest({ agent }) awaits the whole
turn and returns one JSON object — sessionId, status,
output, yieldedTools when it paused, and optionally
events, state and usage.
app.handler.turn({ agent }) hands you the raw ADK event stream if you would
rather define your own wire format. All three commit the session after every turn, so a run
that pauses is durable the moment the request ends.
Recipe 5
Drive it from a terminal
The ADK ships an interactive terminal UI: app.cli(runnable), optionally with a
first message. It is not a command you install — it is a call your program makes, so the agent
you run in it is the one you ship.
// chat.ts — the agent from recipe 1, with a terminal in front of it.
app.cli(assistant, {
input: 'What is 731 * 268, minus 17?',
// Default is false: the UI stays open to be read after the run.
options: { exitOnComplete: true },
})
Its React/Ink dependencies are optional peers, so install them beside the ADK:
npm install ink ink-text-input react
npx tsx chat.ts
It takes over the alternate screen and offers three views, switched by a single key:
debug [d] — every event as it happens, the default;
content [c] — just the conversation; logs [l] — anything the
program wrote to the console, captured rather than smeared over the UI. Arrow keys and
PageUp/PageDown move through the trace; in the debug view Enter or space opens the selected
event's detail pane (r for its raw payload, c for the readable one);
and Ctrl+C leaves.
The reason to reach for it over console.log is the pause. When a yielding tool
stops the run, i opens an input built from that tool's yieldSchema —
you answer the agent in the terminal and the run resumes, which is the sample's
approve without the second process. The call returns a handle you can
await for the RunResult, and it carries the runner and
session it used. The whole surface is the terminal UI.
Recipe 6
The errors you will actually see
Three of these account for most first-hour dead ends. Each message below is the package's own text, verbatim.
No key. Thrown from app.run at the first model call — not at
import, and not when the agent is built. The adapter reads the environment when it needs an
endpoint, so exporting the key in the same shell after the process started will not help; the
process has to start with it.
No OpenAI API key configured.
Set one of these environment variables:
- OPENAI_API_KEY (Standard OpenAI)
- OPENAI_EU_API_KEY (OpenAI EU region)
- AZURE_OPENAI_ENDPOINT + AZURE_OPENAI_API_KEY (Azure OpenAI)
A missing optional peer. The core entry pulls in no provider and no backend, which is the point — and the cost is that each surface you reach for may want an install. Some say so in the ADK's own words; several do not, and surface as Node's ordinary module-resolution failure instead. The map:
| What you used | What to install | What a miss looks like |
|---|---|---|
@animahealth/adk/openai, /gemini, /claude
|
nothing to run — each subpath bundles its provider SDK; a TypeScript project installs
it anyway, or sets skipLibCheck
|
types only — the subpath's declarations reference the SDK's types, so
tsc without skipLibCheck wants openai or
@google/genai. The deprecated core-entry factories (import { openai } from '@animahealth/adk') are the one path that loads the real SDK at runtime, and they need it installed.
|
@animahealth/adk/cli or app.cli() |
ink ink-text-input react |
a module-resolution error at import — no ADK message |
app.handler.agui() |
@ag-ui/core |
a module-resolution error on the first request, when the adapter loads |
@animahealth/adk/stores/sqlite |
better-sqlite3 |
a module-resolution error on the first read or write, not at import |
app.tools.fetchPage() |
jsdom @mozilla/readability turndown, plus playwright for
render: true
|
quiet: the tool returns { success: false, error: 'network_error' } and
the model reads that as a broken page
|
app.mcp (tools from an MCP server) |
@modelcontextprotocol/sdk |
MCP SDK not found. Install it with: npm install @modelcontextprotocol/sdk
|
A model name that does not exist. The ADK does not keep a list of model
names: openai('…') is a descriptor, and the string goes to the provider as you
typed it. So a typo is not caught locally — it comes back as the provider's own error at the
first call. The adapter's only opinion about it is where to go next: an error whose message
contains model not found or deployment not found is treated as worth
retrying at the next configured endpoint, alongside rate limits, timeouts and 5xx.
With one endpoint configured there is no next one, so the provider's error is what you see. On
Azure the name you write is mapped to a deployment name, so check the
modelMapping before you blame the model — the
models chapter covers that mapping.
Two more with their own messages, both thrown early rather than mid-run. Building a web-search tool with no Serper key fails while the agent is being assembled:
No Serper API key configured.
Set the environment variable:
- SERPER_API_KEY (Get one at https://serper.dev)
Or pass directly to webSearch:
webSearch({ provider: new SerperProvider('your-api-key') })
And the Bookings sample refuses to call a model without a key, pointing at the flow that needs
none: OPENAI_API_KEY is not set. Run `npm test` for the same flow, scripted.
Where to go next
The chapters behind these recipes
Each recipe here is the shortest honest version of a chapter. Stopping to ask is the pause the sample is built on; where a sleeping agent lives is the store you swap SQLite for; serving it is the handler surface behind recipe 4; streaming and cost is what those events cost; testing agents and measuring agents are how the thing you built at hour one survives hour ten. When something behaves in a way no chapter explains, the package's source is the answer — it is the same code this page just ran.