Akan.js
Docs
DocsConventionsReferencesCheatsheet
Akan.js
DocsConventionsReferencesCheatsheet
Akan.js

Released under the MIT License

Official Akan.js Consulting onAkansoftCopyright © 2026 Akan.js All rights reserved.System managed bybassman
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▾
AuthorizationOAuth For AgentsSchema DesignText SearchEdge ComputingFile ManagementSingle Sign-OnDataList & Enum
Interface▾
CRUDEndpointMCP ServerAgent ChatForm
Observability▾
LoggingDependency InjectionError HandlingMetrics
Performance▾
CachingImage OptimizationLazy LoadingQueryingMutatingQueueingRealtime
Mobile▾
SetupPush NotificationsDeep LinksUI & KeyboardDesktop Release
Development▾
DocumentationSchema DocsScriptConsoleDockerKubernetesPWATesting
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▾
AuthorizationOAuth For AgentsSchema DesignText SearchEdge ComputingFile ManagementSingle Sign-OnDataList & Enum
Interface▾
CRUDEndpointMCP ServerAgent ChatForm
Observability▾
LoggingDependency InjectionError HandlingMetrics
Performance▾
CachingImage OptimizationLazy LoadingQueryingMutatingQueueingRealtime
Mobile▾
SetupPush NotificationsDeep LinksUI & KeyboardDesktop Release
Development▾
DocumentationSchema DocsScriptConsoleDockerKubernetesPWATesting
PreviousMCP ServerNextForm

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
holds no session, runs no tool
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
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
  • instructions is English, always. The model reads it whatever language the shop sells in, and the same holds for every .desc() and Agent.Guide.
  • title and intro go through l(). l() is for strings a person reads, which is why instructions does not use it.
  • persist keeps 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.
launcherboolean
attach, voice, visual, maxTurns, compact and the rest are listed in the Agent UI reference.
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.

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 and unread for a new-message badge.
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.

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.
  1. Declare the tool with st.tool(name), a .desc(), and the .arg()s the model passes.
  2. End the chain with .card(render) instead of .exec(fn). The call parks in the chat and your form renders there.
  3. In the form, call submit(value) to answer or cancel(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

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 , an object of five fields:

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 AgentQueued card 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

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 does:

On this page

Where A Turn Runs
Mounting The Chat
Every Part Is A Slot
A Tool The User Answers
Pointing At Data
The Queue And The Slash Menu
Keeping The Transcript
  • Keep a chat on its screen with builtins={["readScreen", "readState", "highlight"]}. Without navigate and goBack it 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 builtins leaves it alone.
  • askUser is not on this list. It belongs to the session, so builtins never 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.
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.
  • AgentChat is 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.
  • isRunning is 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. DefaultSteps draws the same flat bubbles into a Fragment, so it takes no className and no existing layout notices it.
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.
ReferenceSource
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.
  • type decides what leaves the browser. It uses st.expose's vocabulary: the value is masked by the model class you name, so its hidden, secret and visual fields never travel.
  • Name the class that carries the field. A Light class usually does not, and a reference masked by one arrives without the field that was the reason for pointing.
  • search needs a query the browser may call. Here it is a slice wrapping a q.search() filter, which belongs only on data that is safe to enumerate.
  • Mount this chat from a client component. reference holds functions, which a server layout cannot pass, so the chat lives in a small component under ui/.
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.Zone or an AgentProvider. A root Agent.Chat beside children does not share its session, and outside a zone the call only logs a warning.
  • path names 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.
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 /word that 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; /retry and /compact answer 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 /help text appended plainly would come back next turn as something the assistant believes it said.
  • Reaching the session. Agent.Zone's onSession hands it over, useAgent() reads it inside a zone, and a composer override receives it as the session prop.
  • 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.
Agent.Guide
apps/koyo/lib/icecreamOrder/IcecreamOrder.Zone.tsx
  • It needs an enclosing session. Put Agent.History inside an Agent.Zone or AgentProvider; outside one it throws while rendering.
  • Session options go on the zone. Inside an Agent.Zone the chat binds to the zone's session, so persist, builtins and instructions set on that Agent.Chat are ignored.
  • onCompact follows 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.
In-Page Agent→
The agent's surface: st.tool actions, readable keys, zones and LLM adaptors.
Agent UI Reference→
Every prop of Agent.Chat, Agent.Zone, Agent.History and the rest.
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
default true
false draws no floating button, for a shell that already has its own entry point.
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.