| concept | AgentSurface | |||
|---|---|---|---|---|
| status | stable | |||
| tracked_sources |
|
|||
| related |
|
|||
| last_verified | 2026-04-29 |
AgentSurface is a typed, serializable description of a UI render request
emitted by a plugin (or an agent) when it needs human input or wants to
display a structured result. Variants are sealed: Form (multi-field
input), Choice (single/multi-select picker), Confirmation (accept /
reject with severity), Card (slot-based rich content). Every variant
carries a correlationId so the emitter can await the matching
AgentSurfaceResponse without coupling to bus internals.
Platform renderers — Compose Multiplatform on Desktop / Android, native UI
on iOS, possibly text on CLI — translate each variant into local UI. The
contract lives in commonMain and intentionally references no platform
or framework types.
A plugin running in commonMain code cannot know what the host platform is. If the contract for "show the user a confirm dialog" leaked any Compose, SwiftUI, or terminal-rendering reference, plugins would either need to import it (impossible across platforms) or every platform would need a parallel plugin API (combinatorial). Instead, the plugin describes what it wants the user to do and the platform decides how to render that.
Three design pressures:
- Plugins must be platform-agnostic. Treat the surface contract as the entire plugin↔host UI vocabulary. New surface kinds require a commonMain change followed by per-platform renderer changes — never one without the other.
- The model can't draw UI. An LLM can't render pixels, but it can choose which surface to emit and fill in the fields. Sealed variants constrain the choice to four well-understood shapes.
- Responses are paired, not pushed.
correlationIdmakes a request and its response a transactional unit. Plugins useawaitSurfaceResponse(correlationId)to suspend until the user replies. Without correlation, async UI events would race.
agents/events/surface/AgentSurface.kt— the sealedAgentSurfaceinterface and four variants.agents/events/surface/AgentSurfaceField.kt— typed fields forForm(Text, Number, Choice, Date, …).agents/events/surface/AgentSurfaceResponse.kt— the response shape, also keyed bycorrelationId.agents/events/surface/AgentSurfaceBusExt.kt—awaitSurfaceResponseextension.agents/domain/event/AgentSurfaceEvent.kt— the bus event carrying a surface request.docs/ampere/agent-surface.md— design doc with renderer guidance.
- No platform types in the contract.
AgentSurfaceand its variants reference onlykotlinx.serialization,commonMaintypes, and primitives. AComposable,UIView,View, or terminal type appearing in this package is a violation. correlationIdpairs request and response. Every variant requires one; everyAgentSurfaceResponsecarries the same id. Renderers must propagate it. Generating new ids on the response side defeats the pairing.- Variants are sealed. New surface kinds extend the sealed hierarchy; plugins can't define their own. This is what makes per-platform renderers exhaustively switchable.
- Surfaces are emitted via the bus, not direct method calls.
AgentSurfaceEventcarries the surface to the platform layer. A plugin reaching directly into a renderer skips logging, replay, and trace. - Field constraints in
Formare validated by the renderer, then again by the response handler. Don't trust the response; validate twice. Card.Slotis sealed. New slot kinds add a sealed variant — they don't introduce aCustom(any)escape hatch.
- Ask for a typed input — emit
AgentSurface.Form(correlationId, title, fields = listOf(AgentSurfaceField.Text(...))).awaitSurfaceResponse(correlationId)returns the typed field map. - Ask the user to pick —
AgentSurface.Choice(...); setmultiSelect = truefor multi-pick, otherwise it's single-select. - Confirm a destructive action —
AgentSurface.Confirmation(... severity = Severity.Destructive)so the renderer can style accordingly without leaking renderer types. - Display a structured result —
AgentSurface.Card(title, slots = listOf(Slot.Heading(...), Slot.KeyValue(...))). - Add a new surface kind — extend the sealed
AgentSurface(andSlotfor cards). Add a renderer for each platform; CI must show all renderers updated together.
- Emitting strings or markdown when a typed surface fits.
"Please enter your email:"printed to a log loses the structure that lets the platform render properly and lets the trace show what the user was asked. - Generating a new
correlationIdin the renderer. Now the request and response don't pair, andawaitSurfaceResponsehangs forever. - Importing Compose / UIKit / terminal types into
agents/events/surface/. Even "for one helper". Once it's in commonMain, every platform inherits it (or fails to compile). - Using
AgentSurface.Cardslot escape hatches likeBody(text = "<html>...")to embed arbitrary markup. Slots are typed for a reason; HTML inBodydefeats per-platform rendering. - Branching renderer behaviour on string-matched titles. "If title contains 'destructive' show red." Use
SeverityonConfirmationinstead — that's what it's for. - Skipping the bus: passing an
AgentSurfacedirectly to a renderer instance. BypassesAgentSurfaceEvent, so the trace can't show the user prompt and the recall layer can't see what was asked.