YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.
General▾
Interface▾
Observability▾
Where A Turn Runs
Agent.Chat adds a chat that drives your screens for the user. The server only relays messages; every tool runs in the user's own browser tab.Words used on this page
TermDescription
turn
Everything the agent says and does between one user message and the next.
transcript
The conversation so far, kept in the browser tab and sent to the model every turn.
relay
The
runAgentTurn endpoint, which passes the transcript to the LLM and never runs a tool.tool
One action a component publishes with
st.tool, usually the handler its button calls.approval card
A card that holds a call until the user approves it.
slot
One part of the chat you can replace in
_overrides.tsx.reference
Data the user pointed at with
@, carried inside their message.zone
A section wrapped in
Agent.Zone, with a conversation of its own.One refund, traced
A customer on the order screen types “refund the last one”, and a moment later the order is refunded. These are the questions a security reviewer asks first:
| Question |
|---|
| ↳ Answer |
| Which machine ran the refund? |
| The customer's browser tab, through the handler the Refund button calls. |
| Whose credential did it carry? |
| The signed-in user's own, exactly like a click on that page. |
| What kept it off someone else's order? |
| The guards every call passes, plus the approval card when the tool asks for one. |
| What did the server do? |
runAgentTurn forwarded the transcript and tool descriptions and returned one answer. |
| What did the server keep? |
| Nothing: it holds no session and stores no transcript. |
One turn, end to end
Screenst.tool declarations · subscribed keys
Agent.Chatthe transcript lives in this tab
POST runAgentTurnguarded by AgentRelayAccess
LLM providernamed in option.setLlm
The tool calls the model asked for
Approval cardconfirm and guard
The tool runs in this browserthe handler the button calls
Change reportwhat moved on screen
Nothing is stored server-side
Screenst.tool declarations · subscribed keys
Agent.Chatthe transcript lives in this tab
POST runAgentTurnguarded by AgentRelayAccess
holds no session, runs no tool
LLM providernamed in option.setLlm
Nothing is stored server-side
The tool calls the model asked for
Approval cardconfirm and guard
The tool runs in this browserthe handler the button calls
Change reportwhat moved on screen


Name a guard, or the chat answers no one.
AgentRelayAccess refuses every caller until lib/option.ts calls option.setAgentAccess(SignedIn) with your own guard. It takes the same guards any endpoint names, ANDed when there are several.Mounting The Chat
Mount
Agent.Chat once, in the layout that wraps every screen the agent should reach. That one element brings the launcher, the transcript, the approval card and the streaming loop:apps/koyo/page/(shop)/_layout.tsx
instructionsis English, always. The model reads it whatever language the shop sells in, and the same holds for every.desc()andAgent.Guide.titleandintrogo throughl().l()is for strings a person reads, which is whyinstructionsdoes not use it.persistkeeps the conversation across reloads. The last section covers where it is kept.


Do not mount it conditionally to close it. Unmounting aborts the session and throws the conversation away. To open the chat from your own control, pass the controlled pair
open and onOpenChange.Props
titlestringdefault l("base.agent")
Header text and the panel's accessible name.
instructionsstring
App-wide guidance for the model, in English, which
Agent.Guide adds route guidance to.defaultOpenbooleandefault false
Opens the panel on first render while the panel owns its state.
openboolean
Controlled open state, paired with
onOpenChange; left off, the panel owns it.onOpenChange(open: boolean) => void
Called on open and close; without it, a controlled panel draws no close button.
launcherbooleandefault true
false draws no floating button, for a shell that already has its own entry point.launcherPlacement"right" | "left"default "right"
The button's corner, kept above a tab bar or composer and the safe area, and under menus and modals.
hideLauncherBelow"sm" | "md" | "lg"
Hides the button below this breakpoint; ⌘L / Ctrl+L or a controlled
open still opens the panel.introReactNode
Replaces the empty-state line while the transcript is empty, where starter questions go.
headerReactNode
Extra controls in the header bar, left of the built-in clear and close buttons.
chromebooleandefault true
false drops the header bar and header for an inline chat; /new still clears.defaultDraftstringdefault ""
The composer's opening text, read once at mount and never sent, where a
?prompt= value goes.inlinebooleandefault false
Renders in the page flow instead of floating, for a zone chat inside its own section.
shortcutbooleandefault true
⌘L on Apple platforms and Ctrl+L elsewhere;
false gives the chord back to the browser.launcherClassNamestring
Classes for the closed button only, where
className reaches both surfaces.panelClassNamestring
Classes for the open panel only.
builtinsboolean | AgentBuiltin[]default true
Which built-in tools this chat's agent gets: all, none, or exactly the ones listed.
persistPersistOption | SessionHistory
Keeps the transcript across reloads, as the last section shows.
Five built-in tools
Besides what the screen declares, the runtime gives every chat these five tools.
builtins picks which ones this chat's agent gets.ToolDescription
navigate
Opens an internal path through the same router
Link uses.goBack
Returns to the previous page in this session's history.
readScreen
Reads the rendered screen as compact text.
readState
Reads one store key the screen subscribed, masked by its model.
highlight
Scrolls one thing into view and flashes it, to show the user where it is.
- Keep a chat on its screen with
builtins={["readScreen", "readState", "highlight"]}. WithoutnavigateandgoBackit cannot leave. - A withheld tool does not exist for the model. Calling it anyway answers "unknown tool", the same as a name that was never registered.
- A screen's own tool is never withheld. A tool a component declares under a built-in's name belongs to the screen, so
builtinsleaves it alone. askUseris not on this list. It belongs to the session, sobuiltinsnever removes it.- There is no general wait tool. One was built and removed because, knowing no key's meaning, it parked turns on whatever key looked promising; declare a waiting tool beside the control that starts the work instead.
Every Part Is A Slot
A brand rarely wants the framework's bubble, but always wants its approval gate. So you replace parts, not the chat: twelve slots bind in a
page/**/_overrides.tsx manifest and cascade down the route tree like layouts.SlotDescription and default export
AgentChat
The whole panel (launcher, transcript, cards and composer), so reach for it last.
AgentLauncher
The closed-state button, given
label, hotkey, unread for a new-message badge, and style carrying its layer, which it spreads onto its root as it does className.AgentBubble
One message; wrap yours in
memo(), since the transcript re-renders on every delta.AgentSteps
One whole agent turn plus
isRunning; the default adds no element.AgentComposer
The input row with Send and Stop, whose field shell comes from the
input recipe slot.AgentApproval
The confirm gate above the composer, which a
remove* tool reaches by default.AgentQuestion
The
askUser card, which shows the choices while a free-text answer goes in the composer.AgentQueued
The message parked while a turn runs, with its take-back and drop controls.
AgentMenu
The completion list above the composer:
/ commands and @ references.AgentMarkdown
Assistant text, built as React elements and never with
dangerouslySetInnerHTML.AgentCode
A fenced code block in that text, where a highlighter binds;
lang is the fence's language.AgentToolCard
The frame around a card tool's own component.
- Compose the default. Eleven slots export their default beside them, so a replacement can wrap the one it replaces instead of rewriting it.
AgentChatis the last resort. It replaces the whole panel and has no exported default to compose.
Folding a turn with AgentSteps
AgentSteps is the one slot that is not a re-skin. It receives a whole turn, so it can fold the steps into a <details> and leave the final answer outside:apps/koyo/ui/KoyoTurn.tsx
Then bind it, with any other slots, in the route's manifest:
apps/koyo/page/(shop)/_overrides.tsx
- A turn is the unit only this slot sees. Neither message on either side of a turn boundary knows it sits at an edge, so a per-message slot cannot fold one.
isRunningis true only for the last turn while the session works on it. Without it, a live progress line and a finished turn's header look the same.- The default adds nothing.
DefaultStepsdraws the same flat bubbles into a Fragment, so it takes noclassNameand no existing layout notices it.
A Tool The User Answers
Some arguments are the user's to give: a delivery address, a phone number, a date someone has to look up. A model that fills them in has answered its own question, and prompting cannot reliably stop it.
- Declare the tool with
st.tool(name), a.desc(), and the.arg()s the model passes. - End the chain with
.card(render)instead of.exec(fn). The call parks in the chat and your form renders there. - In the form, call
submit(value)to answer orcancel(reason)to decline.
Here the model asks the customer for an address, and the Deliver button stays off until one exists:
apps/koyo/lib/icecreamOrder/IcecreamOrder.Zone.tsx
How a card differs from an exec
submit(value)
What the form submits is the call's result, which the model reads back.
cancel(reason) arrives as an error instead, so a dismissed card is something the agent can respond to.Arguments Are Checked First
Before the card is parked, never while it renders. A bad argument reaches the model as a refusal it can fix; a throw in your component would take the chat panel down.
It Waits Outside The Tool Queue
A form in front of a person is not work. Holding the execution lock through it would freeze every other agent on the page behind one unanswered card.
confirm
Not read for a
.card() at all. The card in front of the user is already the asking.- The frame always draws a dismiss, even when your form has no cancel button, so a turn never parks on something the user cannot get out of.
- Store writes are still reported. The screen is snapshotted before and after the wait, so a card that writes what it collected into the store reports what moved like any other call.
Pointing At Data
“Why was this one refunded?” can only be answered if the chat knows which one. The @ menu lets the user point at it, instead of typing an id or making the agent spend a turn searching.
A Whole Document
Declared on the chat's
reference prop. The @ menu finds rows with your own search.<Agent.Chat reference={[…]} />One Field On Screen
Called from the component that draws the field. It hands over the value it already holds, with no round trip.
useAgentReference()Whole documents in the @ menu
Which documents a user may point at is the app's answer, not the framework's, so each source brings its own search:
apps/koyo/ui/KoyoAgentChat.tsx
Each source is a
ReferenceSource, an object of five fields:refNamestring
Your own model name, the vocabulary your published tools already speak.
labelstring
What this group of rows is called in the
@ menu, so pass it through l().typeAgentFieldType
The model class that masks the value before it leaves the browser.
search(query, signal) => Promise<ReferenceCandidate[]>
Your query for the menu rows, whose
signal aborts when the user keeps typing.resolve(refId) => Promise<unknown>
Loads the document, once, when the user picks a row.
typedecides what leaves the browser. It usesst.expose's vocabulary: the value is masked by the model class you name, so itshidden,secretandvisualfields never travel.- Name the class that carries the field. A
Lightclass usually does not, and a reference masked by one arrives without the field that was the reason for pointing. searchneeds a query the browser may call. Here it is a slice wrapping aq.search()filter, which belongs only on data that is safe to enumerate.- Mount this chat from a client component.
referenceholds functions, which a server layout cannot pass, so the chat lives in a small component underui/.
One field with useAgentReference
The component that draws a field already holds its value. It is also the only thing that knows a rich-text field stored as
field(Any) reads as a paragraph, not as an editor document:apps/koyo/lib/icecreamOrder/IcecreamOrder.Util.tsx
- It needs a session above it, so render the button inside an
Agent.Zoneor anAgentProvider. A rootAgent.Chatbesidechildrendoes not share its session, and outside a zone the call only logs a warning. pathnames the field. It is a dotted path into the document, so two fields of one document are two separate references.
In the composer
- Pointers read as names. Where sources are declared, a Lexical editor loaded as its own chunk shows each pointer as the name it points at, not as the raw
@[label](mention:…)token. mentions={false}keeps the plain textarea. Use it when you override the composer or want to see the tokens you send; the draft string is identical either way.- The token carries the reference. Deleting it drops the reference just as removing the chip does, and a token pasted from an earlier message travels as a pointer with no value.


A reference is a snapshot, capped at 20,000 characters. Past that, the JSON is clipped mid-structure and a note tells the model so. Unlike a tool result, which answers one turn, it rides every turn after its message and is the last thing compaction folds.
The Queue And The Slash Menu
A turn takes seconds, and a user who thinks of the next thing should not have to wait to type it. Enter during a turn parks the message and sends it the moment the turn ends.
- One slot. A second send joins the first on a new line, so the model receives one user message, not two.
- It is shown, not held silently. The
AgentQueuedcard above the composer offers take-back and drop, since a send that left the composer but is not in the transcript would read as lost. - Stop means stop. Stop hands the parked message back to the composer instead of opening the next turn with it.
Slash commands
The / menu lists these six commands and nothing else:
CommandDescription
/new/clear
Starts a new conversation, even mid-turn or over an open question card.
/retry
Resends the last user message and leaves everything above it in place.
/compact
Summarizes the whole conversation now, keeping nothing verbatim.
/copy
Copies the transcript as markdown with the page URL and time; local notes are left out.
/help
Lists the commands as a local note that is never sent to the model.
/tools
Lists the tools and readable keys this screen published; a zone chat lists its zone only.
- An app cannot add a / command. A product's own reusable request is a
page().prompt(), which MCP clients list and the in-page chat does not. - Anything else is a message. A
/wordthat is not one of the six is sent to the model as ordinary text. - Commands run mid-turn. They work while a turn is in flight or a question card is open;
/retryand/compactanswer that the agent is busy.
Keys in the composer
KeyDescription
Enter
Sends, parks the message during a turn, or picks the row when a menu is open.
Shift+Enter
Adds a new line.
↑ / ↓
Walks what was sent from the first or last line, or moves the selection in an open menu.
Tab
Completes the selected / command, or picks the selected @ row.
Esc
Hides an open menu; otherwise closes the panel.
⌘L / Ctrl+L
Opens the chat and focuses the composer, unless
shortcut={false}.Session calls behind the menu
CallDescription
session.note(text)
Writes a local note: shown in the transcript, never sent to the model.
session.report(error)
Records a host-side failure, such as a command that threw, as an error in the transcript.
session.retry()
Resends only the last user message and keeps everything above it.
- Why a note and not a plain message. The transcript is the model's history, so
/helptext appended plainly would come back next turn as something the assistant believes it said. - Reaching the session.
Agent.Zone'sonSessionhands it over,useAgent()reads it inside a zone, and a composer override receives it as thesessionprop. - With
persist, recall survives a reload. The ↑/↓ list is seeded from the restored transcript, and the half-written draft you walked away from comes back at the end of the walk.
Keeping The Transcript
The relay keeps no session, so the conversation lives in one browser tab and nowhere else.
persist keeps it across reloads, and Agent.History keeps it on your server.| Write |
|---|
| ↳ How it is kept |
| persist |
sessionStorage, which survives a refresh but dies with the tab, so no shared PC keeps it. |
| persist={{ storage: "local" }} |
localStorage, for a conversation that should outlive the tab. |
| persist={{ key: "…" }} |
Your own storage key; the default is akan.agent.<appName>, plus the zone path in a zone. |
| <Agent.History /> |
| Your own server, through three functions you write. |
On your server: Agent.History
A server store is a
SessionHistory of three functions. A function cannot cross the RSC boundary as a prop, so passing it would make every ancestor up to the session's owner a client component. Instead it mounts as a leaf component inside the zone it keeps, the way Agent.Guide does:apps/koyo/lib/icecreamOrder/IcecreamOrder.Zone.tsx
- It needs an enclosing session. Put
Agent.Historyinside anAgent.ZoneorAgentProvider; outside one it throws while rendering. - Session options go on the zone. Inside an
Agent.Zonethe chat binds to the zone's session, sopersist,builtinsandinstructionsset on thatAgent.Chatare ignored. onCompactfollows compaction. It is called after a compaction replaced messages with one summary, which is where a host with its own server-side summary moves its watermark.
What each store keeps
Rule
persist
Agent.History
Every store
Restores into an untouched chat only
✓
✓
Mounted with the zone, it restores; mounted later, it only saves from then on.
Saves after every change
✓
✓
Debounced and one save at a time; a failed save is silent.
Web storage only
Keeps the newest 50 messages
✓
Drops file bytes and reference values
✓
A file keeps its name, type, url and ref; a reference keeps its pointer and a note to read it again.
✓AppliesDoes not apply
Web storage holds a few megabytes and one screenshot fills much of it. A failed save is silent, so storing file bytes would quietly stop saving the transcript.
Agent.History receives the messages as they are, content included.Read next
What the chat can do on these screens is a separate subject: a component declares one action with
st.tool and makes one store key readable by reading it, and nothing is derived from a store class.