Travel workspace
Connect a generated agent to an itinerary, map, and composer.
The homepage example connects the Trip Planner agent to a small travel workspace. The agent fills two live bindings and calls three typed tools. Your app supplies the flight inventory, map, Markdown renderer, and share UI.
Set up the client
Follow the quickstart to install the SDK and mint a user-scoped session on your server. Publish your workspace's Trip Planner seed and generate a client for that published agent/version. The typed-client guide covers CLI setup and credentials.
Set INLAY_AGENT_ID and INLAY_AGENT_VERSION to your own published agent:
inlay codegen \
--agent "$INLAY_AGENT_ID" --version "$INLAY_AGENT_VERSION" --name Trip \
--out inlay/trip.generated.ts
# Use the same file, agent, and version in CI.
inlay codegen \
--agent "$INLAY_AGENT_ID" --version "$INLAY_AGENT_VERSION" --name Trip \
--out inlay/trip.generated.ts --checkThe seed's revision loop makes codegen conservatively render the specialist feed region flat. Its tool and binding types are still generated; the example below reads the bindings directly. See generated feeds for custom transcript rendering.
The workspace
Create app/travel-workspace.tsx with these imports. The @/travel/ui module
contains the adapters and components shown in the following sections.
"use client";
import type { Session } from "@inlayai/sdk";
import { useTripAgent } from "@/inlay/trip.generated";
import { Itinerary, ShareSheet, TripComposer, TripMap, useTravelUI } from "@/travel/ui";export function TravelWorkspace({ session }: { session: Session }) {
const travel = useTravelUI();
const agent = useTripAgent({
session,
tools: {
searchFlights: travel.searchFlights,
showOnMap: travel.showOnMap,
shareTrip: travel.openShareSheet,
},
});
const itinerary = agent.useBinding("itinerary");
const highlights = agent.useBinding("highlights");
return (
<section aria-label="Travel workspace">
<div className="grid gap-6 lg:grid-cols-2">
<TripMap ref={travel.map} />
<Itinerary markdown={itinerary?.value} highlights={highlights?.value} />
</div>
<ShareSheet ref={travel.share} itinerary={itinerary?.value} />
<TripComposer agent={agent} />
</section>
);
}There is one agent instance. The composer receives that handle, and the share sheet receives the same itinerary value displayed beside the map.
Tool keys match the names authored in the graph. This seed names its tools
searchFlights, showOnMap, and shareTrip; codegen preserves those names.
App-owned UI and tools
In travel/ui.tsx, import the React helpers and generated types used below:
"use client";
import { useRef, useState, type ComponentType, type RefAttributes } from "react";
import type { TripAgentHandle, TripBindings, TripToolHandlers } from "@/inlay/trip.generated";These declarations describe your app's integrations. Replace the declarations
with imports of your implementations, and re-export TripMap and ShareSheet
from the UI module for the workspace to use.
declare const searchFlights: TripToolHandlers["searchFlights"];
// showPlaces resolves the supplied names/day numbers, updates the app's map,
// and returns its actual result. It does not receive pre-geocoded coordinates.
type TripMapHandle = { showPlaces: TripToolHandlers["showOnMap"] };
declare const TripMap: ComponentType<RefAttributes<TripMapHandle>>;
declare const Markdown: ComponentType<{ children: string }>;
// The app's review UI reads the current binding through its prop, waits for
// user interaction, and returns the actual sent/cancelled outcome to the tool.
type ShareSheetHandle = { open: TripToolHandlers["shareTrip"] };
declare const ShareSheet: ComponentType<RefAttributes<ShareSheetHandle> & { itinerary: string | undefined }>;searchFlights receives the confirmed origin, destination, departure date, and
party size, plus optional return date and cabin. Return your inventory result or
an explicit unavailable/error result. TripMap.showPlaces resolves the supplied
place names and one-based day numbers using your map integration.
ShareSheet.open opens your review UI. It waits for the traveller to share or
cancel, then resolves with that outcome. The sheet reads its latest itinerary
prop; any browser-native share action happens from the traveller's interaction
inside the sheet. See client tools for the result lifecycle.
The tool adapter
Each workspace gets its own map and share refs. The generated handler types check
the adapter's arguments and return values. React components can expose the two
imperative methods with useImperativeHandle.
export function useTravelUI() {
const map = useRef<TripMapHandle>(null);
const share = useRef<ShareSheetHandle>(null);
const showOnMap: TripToolHandlers["showOnMap"] = (args) =>
map.current ? map.current.showPlaces(args) : { error: "The map is not ready." };
const openShareSheet: TripToolHandlers["shareTrip"] = (args) =>
share.current ? share.current.open(args) : { error: "Sharing is not ready." };
return { map, share, searchFlights, showOnMap, openShareSheet };
}The itinerary
The generated bindings provide a Markdown string and a list of highlight strings. The small live-item guard handles malformed items while a list is still building; the SDK validates the declared item schema when the binding settles.
export function Itinerary({ markdown, highlights }: {
markdown: TripBindings["itinerary"] | undefined;
highlights: TripBindings["highlights"] | undefined;
}) {
return (
<article>
<h2>Your trip, taking shape</h2>
<Markdown>{markdown ?? "Your day-by-day plan will appear here."}</Markdown>
<ul aria-label="Trip highlights">
{highlights?.map((highlight, index) =>
typeof highlight === "string" && <li key={index}>{highlight}</li>,
)}
</ul>
</article>
);
}The composer
The same composer sends the first request, answers clarification questions, and
requests revisions. It uses canSend and canRetry for the controls and displays
the latest assistant reply. Run failures appear in snapshot.error by default.
For its send call, the composer opts into { throwOnFailure: true } so a failed
send reaches the catch and preserves the draft. Retry failures use the snapshot
error display. The hook also supports failures: "reject" when you want rejection
for all sends and retries; see failure handling.
export function TripComposer({ agent }: { agent: TripAgentHandle }) {
const [draft, setDraft] = useState(
"Plan five days in Tokyo, 12–16 April 2027, from London for two. Food, design, and day trips.",
);
const [error, setError] = useState("");
const reply = agent.useTranscript().filter((turn) => turn.type === "assistant").at(-1);
const failure = error || agent.snapshot.error?.message;
async function send() {
if (!agent.canSend || !draft.trim()) return;
setError("");
try {
await agent.send(draft, { throwOnFailure: true });
setDraft("");
} catch {
setError("Could not update your trip. Your request is still here.");
}
}
return (
<>
<p aria-live="polite">{reply?.content}</p>
<form onSubmit={(event) => { event.preventDefault(); void send(); }}>
<label>
Your trip or changes
<textarea value={draft} onChange={(event) => setDraft(event.target.value)} disabled={!agent.canSend} />
</label>
<button disabled={!agent.canSend || !draft.trim()}>Plan or update trip</button>
</form>
{agent.canRetry && (
<button type="button" onClick={() => {
setError("");
void agent.retry().catch(() => setError("Could not retry. Please try again."));
}}>Retry planning</button>
)}
{failure && <p role="alert">{failure}</p>}
</>
);
}All five quoted regions are typechecked against the seed-generated Trip contract and checked for drift. For reconnecting a workspace after navigation or refresh, see conversations and history.