Agent Development Kit · Prove & ship

Test a workflow without model calls.

Build a workflow that shares typed state, gives each agent its own context, and pauses before booking. Run the cells in order, or press Run all. The model responses are scripted; the tools, state updates, context rendering, and pause/resume use the real ADK.

No API key Synthetic booking · no external actions Runs in your browser

Step 1

Define the working data once.

The availability tool proposes a slot. The booking tool waits for a typed approval before changing the booking state. Both tools use the schema declared on the app; their ctx.state fields are inferred from it. This example records a booking as a counter in memory, so it cannot book a real appointment.

import { adk } from '@animahealth/adk'
import { MockAdapter } from '@animahealth/adk/testing'
import { z } from 'zod'

const scripted = new MockAdapter()
const bookingApp = adk({
	name: 'booking-demo',
	schema: {
		session: {
			request: z.string().default('Please find an appointment on Tuesday.'),
			slot: z.string().default(''),
			status: z.enum(['searching', 'proposed', 'booked', 'declined']).default('searching'),
			bookings: z.number().default(0),
		},
	},
	adapters: { openai: scripted },
})

const findSlot = bookingApp.tool({
	name: 'find_slot',
	description: 'Find an available appointment slot.',
	schema: z.object({}),
	execute: (ctx) => {
		ctx.state.slot = 'Tuesday 14:30'
		ctx.state.status = 'proposed'
		return { slot: ctx.state.slot }
	},
})

const bookSlot = bookingApp.tool({
	name: 'book_slot',
	description: 'Ask for approval to book the proposed slot.',
	schema: z.object({}),
	yieldSchema: z.object({ approved: z.boolean() }),
	execute: (ctx) => {
		if (!ctx.input?.approved) {
			ctx.state.status = 'declined'
			return { booked: false }
		}
		ctx.state.bookings += 1
		ctx.state.status = 'booked'
		return { booked: true, slot: ctx.state.slot }
	},
})

const toolNames = [findSlot.name, bookSlot.name]

toolNames

The cell prints the two tool names. Neither tool has executed yet.

Step 2

Share a slot without sharing the whole conversation.

The specialist reads the request from state and sees its own history. The booking assistant receives the proposed slot through typed state, plus its own history. It does not receive the specialist's conversation. A sequence runs the specialist first, then the booking assistant.

const availability = bookingApp.agent({
	name: 'availability',
	model: { provider: 'openai', name: 'scripted' },
	context: [
		bookingApp.context.system(({ state }) => `Find a slot for this request: ${state.request}`),
		bookingApp.context.history({ scope: 'invocation' }),
	],
	tools: [findSlot],
})

const coordinator = bookingApp.agent({
	name: 'coordinator',
	model: { provider: 'openai', name: 'scripted' },
	context: [
		bookingApp.context.system(({ state }) =>
			`Offer ${state.slot}. Use book_slot to request approval. Booking status: ${state.status}.`,
		),
		bookingApp.context.history({ scope: 'invocation' }),
	],
	tools: [bookSlot],
})

const bookingFlow = bookingApp.sequence({
	name: 'booking',
	runnables: [availability, coordinator],
})

bookingFlow.name

Step 3

Stop before the booking happens.

The script supplies the model's decisions. The runtime executes find_slot, writes its state, and yields when the coordinator calls book_slot. Expect yielded_tool, a proposed slot, and zero bookings.

scripted.reset()
scripted.setResponses([
	{ toolCalls: [{ name: 'find_slot', args: {} }] },
	{ text: 'The availability check found Tuesday 14:30.' },
	{ toolCalls: [{ name: 'book_slot', args: {} }] },
])

const pausedBooking = await bookingApp.run(bookingFlow, {
	input: {
		message: 'Please find an appointment on Tuesday.',
		initialState: { session: {} },
	},
})

const waitingBooking = {
	status: pausedBooking.status,
	slot: pausedBooking.state.slot,
	bookings: pausedBooking.state.bookings,
	waitingFor: pausedBooking.session.yieldedTools.map((tool) => tool.name),
}

waitingBooking

Step 4

Inspect the two model inputs.

The scripted adapter records the context it receives. Read the messages below: the coordinator knows the slot through its system message, without receiving the specialist's reply. The full session still records both agents' work.

const modelInputs = scripted.stepCalls.map(({ ctx }) => ({
	agent: ctx.agent.name,
	messages: ctx.events.flatMap((event) =>
		event.type === 'system' || event.type === 'user' || event.type === 'assistant'
			? [`${event.type}: ${event.text}`]
			: [],
	),
	tools: ctx.functionTools.map((tool) => tool.name),
}))

modelInputs

This view shows text messages and available tool names. The Context chapter also shows tool-call and tool-result messages, history scopes, and filters. Context selection controls model input, not access permissions.

Step 5

Supply the answer and inspect the result.

Keep approved true to book. To try rejection, rerun step 3, change approved to false, and run this cell again. Approval produces one booking; rejection leaves zero. The tool checks the decision in code.

const approved = true
const pendingBooking = pausedBooking.session.yieldedTools[0]
if (!pendingBooking) throw new Error('Run step 3 again to create a pending booking.')

pausedBooking.session.input.tool({
	callId: pendingBooking.callId,
	input: { approved },
})
scripted.setResponses([{ text: 'Your decision has been recorded.' }])

const finishedBooking = await bookingApp.run(bookingFlow, { session: pausedBooking.session })

const bookingOutcome = {
	status: finishedBooking.state.status,
	bookings: finishedBooking.state.bookings,
	changes: finishedBooking.session.events.flatMap((event) =>
		event.type === 'state_change' ? event.changes : [],
	),
}

bookingOutcome

The model's closing sentence is scripted. The booking count and state changes come from the tool that actually executed. Tests can assert on those outcomes; live evaluations measure whether a model makes the right decisions.

Continue building

Run it beyond this page.

This example keeps its session in browser memory. To survive a process exit, use a persistent store and commit the session. The Bookings sample demonstrates that boundary with SQLite and separate commands for asking and approving.