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.
CLI Reference▾
AkanJS Reference▾
Agent UI
An assistant that presses the buttons already on the screen, under the same guards, while the person watches. It is not a separate API for robots.
The
Agent namespace is that UI. One <Agent.Chat /> in a layout is the whole integration; the other members narrow it or show what it sees.import { Agent } from "akanjs/ui";

The relay endpoint never runs a tool. Every call runs in the user's own browser session, through the app's guards and the approval card. So a tool exists only where a component declared one, and what the screen does not offer the user, the agent cannot do either.
Words used on this page
TermDescription
tool
One action a component publishes with
st.tool, usually the handler its button calls.surface
Everything the agent can do and read on the current screen: the mounted tools and keys.
session
One conversation with its loop and options.
Agent.Chat and Agent.Zone each build one.transcript
The conversation so far. It is sent to the model again on every turn.
built-ins
Runtime tools every screen gets:
navigate, goBack, readScreen, readState, highlight.resource
A value a component publishes with
st.expose or st.useState for the agent to read.Members
Member
Own UI
Own chat
Name prefix
What the user talks to
Agent.Chat
✓
✓
The chat panel: launcher, transcript, composer and approval card. Mount it once.
Agent.Zone
✓
✓
A section with its own conversation over a narrowed view of the same screen.
Guidance and scope
Agent.Guide
Standing instructions for a route subtree. Renders nothing.
Agent.History
Connects the enclosing zone's transcript to storage the app owns. Renders nothing.
Agent.Skip
A region the default screen read leaves out, named so it can still be asked for.
Agent.Scope
✓
Prefixes the tools and resources below it, without opening a conversation.
Hooks
Agent.useScreenActions
The screen's tools that need no argument, as a list a ⌘K command palette can offer.
Development
Agent.Dock
✓
The development inspector: tools, readable state, withheld keys and the transcript.
Dock parts
✓
Agent.Context, Agent.Section, Agent.StateKey, Agent.Tool, Agent.Transcript: the dock's pieces, for an inspector of your own.✓YesNo
Where the concepts live
This page lists every member and its props. The ideas behind them are explained here:
Chat
The chat people see: a floating panel wired to the tools and state this screen declared. The loop and every tool call run in this browser. Mount it once, in a layout.
Props / API
Session Options
How the conversation runs, read once at mount. Inside an
Agent.Zone or AgentProvider the chat joins that session, so these belong to whoever built it.instructionsstring
App-wide framing. Route guidance from mounted
Agent.Guides layers on top of it.runnerAgentRunnerdefault fetchRunner()
Swaps the transport. The default posts to
runAgentTurn; httpRunner({ url }) posts elsewhere.maxTurnsnumberdefault 12
Model round trips one ask may spend. At the limit, the chat asks the user whether to keep going.
compactCompactOptionsdefault { at: 24_000, keep: 6, buffer: 13_000 }
Summarizes past
at estimated tokens or buffer short of the known window. { at: 0 } turns it off.builtinsBuiltinOptiondefault true
true gives all five built-ins, false none, an array only those named. askUser always stays.persistPersistOption | SessionHistory
Transcript survives reloads in sessionStorage;
{ storage: "local" } or SessionHistory moves it.onCompact(replaced, summary) => void
Runs after a compaction replaced messages with one summary; a host syncs its own watermark here.
visualboolean | AgentVisualOptiondefault true
Rings the calling control (
reveal) and a pointer presses it (cursor). false turns both off.Opening And Placement
defaultOpenbooleandefault false
Starts the panel open. The chat keeps its own open state after that.
open / onOpenChangeboolean / (open) => void
Open state the app controls.
open alone draws no close button.launcherbooleandefault true
false draws no launcher, for an app that opens the panel from a control of its own.launcherPlacement"right" | "left"default "right"
The launcher's corner, above the bottom chrome and safe area, under menus and modals; the open panel stays on top.
hideLauncherBelow"sm" | "md" | "lg"
Draws no launcher below this breakpoint. The shortcut or a controlled
open still opens the panel.inlinebooleandefault false
Renders in the page flow instead of floating, for a zone chat inside its own section.
shortcutbooleandefault true
Cmd/Ctrl+L opens the panel.
false gives the chord back, for a shell that already uses it.Look
classNamestring
Reaches whichever surface is showing: the launcher while closed, the panel while open.
launcherClassName / panelClassNamestring
Styles one surface each, where
className reaches both.titlestring
Panel heading. Left out, it reads "Agent" in the user's language.
introReactNode
Replaces the intro line while the transcript is empty. Starter questions go here.
header / chromeReactNode / booleandefault chrome = true
Controls left of clear and close.
chrome={false} drops the whole bar, leaving /new to clear.Composer Input
defaultDraftstring
Composer text read once at mount, e.g. a
?prompt= value to prefill without sending it.attachAttachReader
Turns a file into an attachment;
null falls back to the built-in reader for images and text.attachLimits{ perFileBytes?, perMessageBytes?, perMessageCount? }
Size and count caps. Defaults: 4 MB per file, 8 MB and five files per message.
reference / mentionsReferenceSource[] / boolean
@ menu sources, each with a search. mentions draws pointers as names, on when sources exist.voiceVoiceEngine
Press-to-talk into the composer. Replies are read aloud only when the question was spoken.
- Closing the panel keeps the conversation. The session lives in a ref, so it survives reopening; without
persistit ends with the page. - Function props need a small client component.
attach,voice,reference,runner,onCompactandonOpenChangecarry functions, which cannot cross the RSC boundary from a server layout. Wrap the chat inui/the wayapps/akan/ui/DocsAgentChat.tsxdoes withuseSpeech()from@libs/util/webkit. - A controlled
openalone stays server-safe. WithoutonOpenChangeno function is passed, so a server component can still render it. - The launcher appears after hydration. The panel is a
lazy(…, { ssr: false })boundary, so its chunk loads once the page has hydrated. That is normal. - Server settings live in
lib/option.ts.option.setLlm({ apiKey, model, host })andoption.setAgentAccess(SignedIn)configure it, never environment variables. - Answer
datawhen the provider cannot reach aurl. The provider fetches anattachresult'surlitself. Answering both sends the bytes to the model and keeps the address for the thumbnail.
A layout mounts it once, with a translated title and a transcript that survives reloads:
apps/koyo/page/(user)/_layout.tsx
Zone
A section with its own conversation over a narrowed view of the same screen. An
Agent.Chat inside binds to it automatically, so two zones on one screen run two conversations side by side, each seeing only its own subtree.Props / API
idstring
Required. Sets the name prefix and
data-agent-zone; characters outside A-Za-z0-9_- become -.childrenReactNode
The section itself. Everything mounted here is part of the zone.
classNamestring
Goes on the wrapper
div that carries data-agent-zone.labelstring
Readable name for the scope, sent to the model with the screen context.
instructionsstring
Zone guidance, mounted as an
Agent.Guide: the root agent reads it too, a sibling zone never.runner / maxTurns / compact / builtins / persist / onCompact / visualsame as Chat
Same contracts as the chat's, applied to this zone's session and read once at mount.
sessionAgentSession
Runs the zone on a session the app built and owns; unmounting the zone leaves it running.
onSession(session) => void
Hands the session out once it exists, for a page or store that sends into it or watches it.
- Zones are views, never walls. Tools,
st.usesubscriptions and guides mounted inside belong to this zone's session and to the root agent both. - Everything a zone publishes is named
<id>.<name>. Instructions that name a tool must carry the prefix; a bare name is a tool that does not exist, and the model spends a turn onUnknown tool. Build the name from the id, and check the published list withAgent.Context's Assemble. - A zone that must stay on its screen withholds the rest.
builtins={["readScreen", "readState"]}takesnavigate,goBackandhighlightaway rather than discouraging them, so no prompt can talk the model past it. - Each zone persists on its own.
persistis keyed by the zone's scope path, so two zones never share a transcript.
apps/koyo/ui/CommentZone.tsx
Guide
Standing guidance for a route subtree. Render it from a
_layout.tsx or a page, and its text joins every turn's instructions while that subtree is mounted. It draws nothing.Props / API
instructionsstring
The text, always in English: the model reads it, so the
l() rule does not apply.- The render tree is the cascade. Each mounted Guide adds its own block, and navigating away withdraws it.
- It is a component, not a route stage. Neither
page()norpageConfighas aninstructionsfield, and*.abstract.mdis never served to agents.
apps/koyo/page/(user)/plan/_layout.tsx
History
Connects the enclosing zone's transcript to storage the app owns. It does what
persist does, as a mounted leaf instead of a prop, and draws nothing.Props / API
load / save / clearSessionHistory["load" | "save" | "clear"]
The three sides of the store. Inline closures are fine, since they are read through a ref.
onCompact(replaced, summary) => void
Where a host with its own server-side summary moves its watermark.
- Why a component rather than
persist. A function cannot cross the server/client boundary as a prop, so passingpersistto whoever builds the session makes every ancestor up to it a client component. With this leaf as the only client module, the zone and its chat stay in a server component. - It needs an enclosing session. Mount it inside
Agent.ZoneorAgentProvider, as in<Agent.Zone id="thread"><ThreadHistory … /></Agent.Zone>. The rootAgent.Chathands no session down, so use itspersistthere. - Restoring happens only on a fresh conversation. Mounted with the zone, it restores; mounted after something has happened, it only saves from then on.
- The store is attached only while this is mounted. A zone's own session ends with the zone anyway, but one the app passed in outlives this and stops saving on unmount. To keep saving, call
session.setHistoryyourself; that takes the slot, so a later unmount here leaves it alone.
apps/koyo/ui/ThreadHistory.tsx
Skip
A region the default screen read leaves out: chrome that costs tokens and answers nothing, such as a footer, a cookie banner or a repeated nav.
Props / API
labelstring
Printed in place of the region, and the name
section takes to read it anyway. Required.childrenReactNode
The region itself.
classNamestring
Goes on the wrapper
div.- The agent knows what it skipped.
[skipped: <label>]stands in its place, so asked about the footer it says it did not read one instead of saying there is none.section: "<label>"reads it on request. - It hides text, not behaviour. Tools and state keys are declarations, not markup: an
st.toolinside is published as before, andhighlightstill reaches a control in here. - Where a wrapper would move the layout, use the attribute. Between a flex container and its children, put it on the element you already render:
<footer data-agent-skip="site footer">.
apps/koyo/ui/SiteFooter.tsx
Scope
Prefixes every tool and resource registered below it, so repeated list items can reuse local names. It opens no conversation and holds no session.
Props / API
idstring
The prefix: everything below is published as
<id>.<name>, nested scopes joined with dots.childrenReactNode
The subtree the prefix applies to.
labelstring
Readable name for the scope, sent to the model with the screen context.
kindstring
What sort of scope this is.
Agent.Zone opens its own with kind="zone".- Scope or Zone? Use
Agent.Scopewhen a repeated subtree needs distinct tool names but shares the screen's one agent.Agent.Zonewraps a scope and adds a conversation of its own.
apps/koyo/ui/WaypointRow.tsx
useScreenActions
The current screen's published tools that take no required argument: what a ⌘K command palette can offer, the same actions the agent can take on this screen. A card tool, which asks the user, is left out.
Props / API
surfaceAgenticSurface
The surface to read. Left out, the screen's shared one, which the chat reads too.
name / descriptionstring / string | undefined
Each action's tool name and
.desc(), as the agent sees them.labelstring | undefined
The tool's
label (st.tool(name, { label }), an l() result): what to show a person, in their language. It is never sent to the model.needsConfirmboolean
true for a remove* tool or one that declares confirm: ask the person before run.run() => Promise<void>
Calls the control's own handler, as its click does, and rejects where the tool's guard refuses.
- A person's pick is theirs.
run()is not recorded as an agent call, and the list follows the screen as its controls mount and unmount.
apps/koyo/ui/CommandList.tsx
Development Dock
Each component declares its own agent surface, so no single file tells you what the whole screen published.
Agent.Dock shows it. Mount it next to the chat during development:apps/koyo/page/(user)/_layout.tsx
- Production draws nothing.
Agent.DockandAgent.Contextrender nothing whenAKAN_PUBLIC_ENV=main, so leaving them mounted costs a visitor nothing. - The other parts do not check the environment. An inspector you assemble from
Agent.Section,Agent.StateKey,Agent.ToolorAgent.Transcripthas to hide itself in production. openexpands Tools. Transcript always starts expanded; the other sections start folded.
What each section answers
SectionDescription
Tools
Did this screen publish what its author meant, under the names the instructions use?
State
Which keys are readable right now, and what does one actually return when read?
Context
What would the next turn carry? Assemble prints the tool names, guides and context blocks.
Withheld
Which keys were refused, and for what reason.
Transcript
What has the agent already done to this page?
The parts
Each part is exported, so an app that wants a dock of its own shape composes them instead of re-reading the surface.
Agent.Dock{ className?, bridge?, surface?, open? }
The whole panel:
bridge supplies the state keys, surface the tools, open expands Tools.Agent.Context{ className? }
An Assemble button that prints what a turn would carry: tool names, guides, context blocks.
Agent.Section{ className?, title, count, children, open? }
One collapsible
<details> group with a count beside its title. The dock draws five.Agent.StateKey{ className?, bridge, name, entry, live? }
One readable key, read and masked on click, so an object no model claims is refused here.
Agent.Tool{ className?, surface, tool, onRun }
One declared tool with its arguments as JSON, and a Run button that calls it in the running app.
Agent.Transcript{ className?, calls }
What the agent did, oldest first, to check against what the page did. There is no undo.
Restyling the chat
Eleven of the chat's own parts are override slots, so an app re-skins the transcript or the composer without re-implementing the loop.
AgentChat replaces the whole panel.AgentLauncherAgentBubbleAgentStepsAgentComposerAgentApprovalAgentQuestionAgentQueuedAgentMenuAgentMarkdownAgentToolCardAgentCode