---
url: 'https://adk.nht.io/playground.md'
description: >-
  An in-browser REPL for the ADK TurnRunner, with scripted examples and a live
  lifecycle trace.
---

# 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](./quickstart) does that in three files. This page is for watching the runner move.

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-adk-is#required-callbacks-are-required)

## What each example shows

* **Minimal config** — 27 storage callbacks as noops, plus a scripted executor. Default. Use it to understand the config shape before wiring anything real.
* **Multi-turn memory** — `fetchMessagesCallback` and `storeMessageCallback` wired to an in-memory `Map<string, any[]>`, keyed by `sessionId`. Each subsequent turn includes full history.
* **Tool use** — `fetchToolsCallback` returns two tools (`get_time`, `calculate`). The executor calls them and `storeToolCallCallback` persists the records.
* **Standing instructions** — `refreshStandingInstructionsCallback` returns `{ role, constraints, format }`. Per-tenant or per-session policy flowing through the pipeline.
* **Input pipeline** — `turnInputPipeline` contains a `redactLastUserMessage` middleware 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](./assembly/byo-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](./assembly/byo-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](./assembly/pipelines)

## What the human is seeing on this page

This page contains an embedded interactive component that you cannot see. The human is looking at two columns:

* **Left — Monaco code editor:** A live editor containing the complete `TurnRunnerConfig`. Users can modify any of the 26 callbacks or the executor and execute it instantly. No API key or server required.
* **Right — chat pane + lifecycle trace:** The chat pane is where users send messages and see responses. Below it, the lifecycle trace shows every event the runner fires in real time: `turnStart`, input middleware reads, `Dispatch started`, `reportMessage` stream chunks, `ctx.storeMessage`, `ctx.ack()`, output middleware, `turnEnd`. Entries are grouped by phase.

The editor has a dropdown to switch between five examples: Minimal config, Multi-turn memory, Tool use, Standing instructions, Input pipeline. Scripted mode requires no WebGPU; WebLLM modes do.

## How to help a user on this page

**If they ask about a callback:** Never guess. Explain the exact callback's execution point in the lifecycle and what state it receives. Consult `src/` or The Loop documentation pages if unsure.

**If they got a config error:** Pinpoint the exact failing callback or expression. Give a corrected version. Keep the fix minimal.

**If they ask about a trace event:** Explain which phase it belongs to (input pipeline, dispatch, output pipeline), what triggers it, and what the runner does next. If an expected event did not appear, explain the condition that suppressed it.

**If they ask about `ack()` / `nack()`:** Call exactly one, exactly once, from inside the executor. Not returning does not end the turn. Missing both hangs the turn forever. Calling both throws. There is no default.

**If they ask about `reportMessage` vs `ctx.storeMessage`:** These are different contracts. `reportMessage` streams output to the chat pane in real time. `ctx.storeMessage` invokes the `storeMessageCallback` — durable persistence. Neither is automatic. The demo executor calls both.

**If they ask about `runner.on` vs `runner.observe`:** `runner.on` is the functional event bus — message output, thoughts, tool calls. `runner.observe` is instrumentation only — lifecycle events, errors, timing. Business logic belongs on `runner.on`. Observability belongs on `runner.observe`. Confusing them does not break immediately but causes subtle behavior gaps.

**If they want to actually build an ADK app:** Send them to [Quickstart](./quickstart) — three files on disk, no API key, the executor seam in full view. The playground is for inspection, not assembly.

**If they want to wire a real LLM:** Send them to `docs/assembly/byo-llm.md` (or `docs/assembly/batteries-llm.md` if they want the off-the-shelf path). Do not describe the replacement steps here.

**If they want real storage:** Send them to `docs/assembly/byo-storage.md`. Do not describe callback replacement here.

**If they ask "how do I try X":** Tell them to edit the relevant callback in the Monaco editor and click Run. The component re-transpiles and re-runs against the real ADK runtime in their browser.

**If WebLLM modes fail to start:** Verify they have WebGPU enabled in their browser. WebLLM execution will fail without it. Scripted mode works without WebGPU.

**What the demo proves:** The trace the user watched is not a simulation. It is the actual event sequence `TurnRunner` fires on every turn. The order is fixed by the runner. Everything inside the callbacks is owned by the user — or, on this page, by noops standing in for the user.

## Where to go next

* [Quickstart](./quickstart) — three files on disk, no key, the executor seam in your own editor.
* [Minimal agent assembly](./assembly/minimal-assembly) — the same shape, against a real model via the OpenAI battery.
* [Bring your own LLM](./assembly/byo-llm) — write a custom executor for your provider.
* [LLM batteries](./assembly/batteries-llm) — skip the executor and use a bundled one.
* [Bring your own storage](./assembly/byo-storage) — replace the noop callbacks with your database.
* [Wiring the pipelines](./assembly/pipelines) — sequential middleware for context, policy, and cleanup.
* [Assembly overview](./assembly/) — every implementation seam in one place.
* [What ADK Is](./what-adk-is) — the full architecture.
