This optional exercise follows mapping, dependencies, and confirmation. Fixed responses let you test expiry, repeated approval, and failure cases without creating real bookings. To connect your own API first, use the real-endpoint quick start.
The scenario is deliberately small: the user supplies a date, the API supplies available slots, the user chooses a time, and the host asks for approval before creating a booking.
The API contract
GET /availability?date=2026-10-01returns{ slots: [{ slot_token, label }] }.POST /bookingsaccepts{ date, slot_token, notes? }.- The POST receives the user's Bearer token and returns
{ booking_id, time }.
These endpoints are a fixture, not a public demo service. The script below intercepts requests, so no network booking is made.
1. Save the complete configuration
Download config.yml. This is a complete file; no fragments from other pages are required.
version: "2"
assistant:
language: auto
timezone: Asia/Hebron
tools:
- id: availability
tool:
description: Find available appointment times for a date. This tool does not create a booking.
input_schema:
type: object
properties:
date: { type: string, format: date }
required: [date]
request:
method: GET
url: https://api.example.com/availability
map:
query:
date: $.date
response:
private_fields: [slot_token]
output_schema:
type: object
properties:
slots:
type: array
items:
type: object
properties:
slot_token: { type: string }
label: { type: string }
required: [slot_token, label]
required: [slots]
- id: book
tool:
description: Book an appointment for a date after selecting an available time and confirming.
input_schema:
type: object
properties:
date: { type: string, format: date }
slot_token: { type: string, minLength: 1 }
notes: { type: string, maxLength: 500 }
required: [date, slot_token]
depends_on:
- tool: availability
when: always
arguments:
date: $.date
select:
items_path: $.slots
id_path: $.slot_token
label_path: $.label
id_sensitive: true
facts:
time: $.label
map:
$.slot_token: $.slot_token
ttl_ms: 30000
behavior:
effect: side-effect
confirmation:
required: true
ttl_ms: 30000
preview:
date: $.date
time: { source: selection, path: $.time }
notes: $.notes
recovery:
refresh_dependency: availability
statuses: [409, 410]
request:
method: POST
url: https://api.example.com/bookings
auth:
type: session
retry:
attempts: 1
map:
body:
date: $.date
slot_token: $.slot_token
notes: $.notes
Follow the data:
book.dateis user-owned. It is forwarded toavailability.date.availabilityretrieves real slots. Its output schema checks every slot has a token and label.selectdescribes how to show those slots.id_sensitivehides their real tokens behind outward-facing IDs.- The dependency map writes the selected token into
book.slot_token. It is absent from the model-facing book schema. - The selection fact
timesupplies a human-readable preview, while the POST body uses the real token. confirmation.requiredpauses before the write.auth.type: sessionforwards the host's token; the API enforces permission.
private_fields removes slot_token from display data. It remains available inside runtime dependency resolution. Privacy projection must not destroy the value needed to execute.
2. Run the host-side fixture
Install the npm package as described in installation, then save this beside config.yml as demo.mjs. Download the script.
import assert from "node:assert/strict";
import { AgentRuntime } from "agento-runtime";
const calls = [];
const runtime = await AgentRuntime.create({
configPath: "./config.yml",
presentation: "raw",
fetch: async (url, options) => {
const path = new URL(url).pathname;
calls.push({ path, method: options.method });
if (path === "/availability") {
return Response.json({
slots: [
{ slot_token: "slot-a", label: "10:00" },
{ slot_token: "slot-b", label: "11:00" },
],
});
}
assert.equal(path, "/bookings");
assert.equal(options.method, "POST");
assert.equal(options.headers.authorization, "Bearer demo-user-token");
assert.deepEqual(JSON.parse(options.body), {
date: "2026-10-01", slot_token: "slot-b", notes: "Window seat",
});
return Response.json({ booking_id: "booking-42", time: "11:00" });
},
});
const request = {
sessionId: "booking-demo",
auth: { token: "demo-user-token" },
};
try {
let result = await runtime.invoke({
...request, tool: "book",
arguments: { date: "2026-10-01", notes: "Window seat" },
});
assert.equal(result.status, "needs_selection");
assert.equal(calls.length, 1);
assert(!result.selection.options.some(option => option.id === "slot-b"));
console.log("1.", result.status, result.selection.options);
// Test fixture: simulate the user choosing 11:00.
const choice = result.selection.options.find(option => option.label === "11:00");
result = await runtime.select({
...request, selectionId: result.selection.id, choice: choice.id,
});
assert.equal(result.status, "needs_confirmation");
assert.equal(calls.length, 1);
assert.equal(result.confirmation.preview.time, "11:00");
assert(!JSON.stringify(result.confirmation.preview).includes("slot-b"));
console.log("2.", result.status, result.confirmation.preview);
// Test fixture only: a real UI waits for explicit user approval here.
result = await runtime.confirm({
...request, confirmationId: result.confirmation.id,
});
assert.equal(result.status, "completed");
assert.equal(calls.filter(call => call.method === "POST").length, 1);
console.log("3.", result.status, result.data);
console.log("HTTP calls:", calls);
} finally {
runtime.dispose();
}
node demo.mjs
The script simulates a choice and approval for this test only. In your product, the choice handler and confirmation handler must wait for the respective user actions.
3. Read the trace
| Step | Result | HTTP calls so far |
|---|---|---|
Invoke book with date and notes | needs_selection with 10:00 and 11:00 | One GET; zero POSTs. |
| Select 11:00 | needs_confirmation with date, time, and notes | Still one GET; zero POSTs. |
| Confirm the returned ID | completed with booking-42 | One GET and one POST. |
The fixture asserts the exact POST body, checks that the real slot token is not in the displayed choices or preview, and checks that only one POST occurs.
4. Make one change at a time
Remove the date. Expect needs_input before any GET. The runtime distinguishes a missing user field from a dependency-produced field.
Return one slot. Without an explicit matching preference, it is selected automatically. The first result becomes needs_confirmation; adjust the fixture assertions accordingly. Automatic selection is not automatic approval.
Return no slots. Expect needs_input with a message explaining no availability. Do not render a generic “missing date” form just because the status is needs_input.
Reduce dependency TTL to 1 millisecond. A choice may expire before the next call. Expect INPUT_SELECTION_STALE or CONFIRMATION_STALE, depending on where expiry occurs. Refresh the flow instead of resubmitting an old ID.
Return 409 once from the POST. The configured recovery clears the stale dependency and looks up fresh slots. Handle the new selection or confirmation; it must not silently confirm a replacement. The second POST should happen only after a new user approval.
Change when to missing and supply a token directly. The lookup can be skipped. Use this only for a trusted host/navigation path, not as a way to validate user-provided tokens.
5. Connect your real API
Replace the two URLs and response schema, then remove the injected fetch. Provide real credentials from your authenticated backend. Keep the schema, selection paths, mappings, and confirmation preview aligned with real response fixtures.
Create one long-lived runtime per application process. Use a stable, authorized conversation ID for every continuation. Do not allow clients to select another user's sessionId.
For a multi-instance deployment, supply shared session storage with per-session transaction serialization and a suitable shared rate limiter. If the API supports idempotency, configure it before enabling write retries. See production readiness.
6. Grow into a service booking
The next increment is book → availability → services: availability needs a service ID, so put the service dependency on availability. Add service labels as selection facts for the preview. Add user preferences only after the basic choice flow passes its tests.
Do not add every feature at once. Establish one observable behavior, test it, then add the next policy.