Playground
A fully interactive REPL for an ADK TurnRunner executing in real time, right inside your browser. Pick the example, pick the model, send a message. That response came from a language model running in the page itself. No API requests, no remote servers, no network hops.
This is not where you start a real ADK application. The 26 callbacks here are pre-wired as noops so you can poke at the execution path without first writing the adapter. That is the trade. If you want to actually build the chassis on disk, the Quickstart does that in three files. This page is for watching the runner move.
import type { TurnRunnerConfig, TurnContext, Message, Memory, Tokenizable } from '@nhtio/adk'
import { LLMExecutor } from 'virtual:adk-demo'
// All 26 callbacks are required — a missing one throws at construction.
// noop/empty are the explicit "no-op" declarations: we mean "no storage here".
const noop = async (_ctx: TurnContext, _val: unknown) => {}
const empty = async (_ctx: TurnContext): Promise<never[]> => []
const config: TurnRunnerConfig = {
// executorCallback: the integration seam. Call ctx.ack() or ctx.nack(err)
// exactly once. Returning without signalling hangs the turn forever.
executorCallback: LLMExecutor,
fetchMessagesCallback: empty,
fetchMemoriesCallback: empty,
fetchThoughtsCallback: empty,
fetchToolCallsCallback: empty,
fetchRetrievablesCallback: empty,
fetchToolsCallback: empty,
refreshStandingInstructionsCallback: empty,
storeMessageCallback: noop,
mutateMessageCallback: noop,
deleteMessageCallback: noop,
storeMemoryCallback: noop,
mutateMemoryCallback: noop,
deleteMemoryCallback: noop,
storeThoughtCallback: noop,
mutateThoughtCallback: noop,
deleteThoughtCallback: noop,
storeToolCallCallback: noop,
mutateToolCallCallback: noop,
deleteToolCallCallback: noop,
storeRetrievableCallback: noop,
mutateRetrievableCallback: noop,
deleteRetrievableCallback: noop,
storeStandingInstructionCallback: noop,
mutateStandingInstructionCallback: noop,
deleteStandingInstructionCallback: noop,
}
export default config
That trace is real. It's the exact sequence of events TurnRunner fires on every turn. The runner owns the sequence — when each phase starts, when each callback runs, when the turn ends. You own what goes inside those callbacks. That is the entire division of labour. On this page the noops are owning it for you.
The 26 callbacks, owned by noops
Yes, 26. Every single one is required. The runner refuses hidden defaults because magic defaults breed production disasters. The REPL examples supply all 26 as noops so you can boot immediately. That is not what a finished application looks like — it is what a finished application looks like with every seam left blank. Don't touch a noop until you are ready to own that exact moment in the lifecycle. → Why every callback is required
What each example shows
- Minimal config — 25 storage callbacks as noops, plus a scripted executor. Default. Use it to understand the config shape before wiring anything real.
- Multi-turn memory —
fetchMessagesCallbackandstoreMessageCallbackwired to an in-memoryMap<string, any[]>, keyed bysessionId. Each subsequent turn includes full history. - Tool use —
fetchToolsCallbackreturns two tools (get_time,calculate). The executor calls them andstoreToolCallCallbackpersists the records. - Standing instructions —
refreshStandingInstructionsCallbackreturns{ role, constraints, format }. Per-tenant or per-session policy flowing through the pipeline. - Input pipeline —
turnInputPipelinecontains aredactLastUserMessagemiddleware that replaces emails with[EMAIL]and phones with[PHONE].
The default mode is scripted and does not require WebGPU. WebLLM modes run the model in your browser and need WebGPU support.
What to change first
The executor
This is where you call your language model. Invoke exactly one of ctx.ack() or ctx.nack(). Exactly once. Returning a value does not end the turn. If you miss both, the turn hangs forever. If you call both, the runner throws. There is no default. → Bring your own LLM
Storage callbacks
No storeMessageCallback means the runner forgets this turn immediately. No fetchMessagesCallback means permanent amnesia on the next one. Wire these to your database when your agent needs to remember. → Bring your own storage
Pipelines
turnInputPipeline and turnOutputPipeline are sequential middleware chains. Use them to wrap the turn with policy: redact PII, check rate limits, moderate content, pack context. Leave them empty until you have a concrete rule to enforce. → Wiring the pipelines
Where to go next
- Quickstart — three files on disk, no key, the executor seam in your own editor.
- Minimal agent assembly — the same shape, against a real model via the OpenAI battery.
- Bring your own LLM — write a custom executor for your provider.
- LLM batteries — skip the executor and use a bundled one.
- Bring your own storage — replace the noop callbacks with your database.
- Wiring the pipelines — sequential middleware for context, policy, and cleanup.
- Assembly overview — every implementation seam in one place.
- What ADK Is — the full architecture.