ask_user tool is the core capability: it
suspends the caller mid-run, records the question, and wakes the caller with the
answer.
The loop
ask_user is the engine-agnostic author surface behind the contract’s AskUser
protocol. Loading it is manifest-driven: opt in the interactions tool module and
the interactions HTTP router, exactly like any other capability.
Answer formats
A question declares one of five answer formats. Four are answered on an authenticated in-client surface — the caller waits while a person responds through the answer door:text— free text.confirm— a yes/no acknowledgement.select— a choice from options.form— a typed payload validated against a declared schema.
external: the human acts on an outside surface — signs a
document, approves a request, pays — and the external system delivers the answer
back through a public callback door. The asking tool blocks exactly as it does for
any other format; only the delivery channel differs.
Media on a question
A question can carry optional media — images and links shown WITH it in the inbox, above the answer controls. Media is display-only: the person still answers through the question’sanswer_format, and the media never becomes part
of the answer. It is orthogonal to the answer format — valid with every one — and
renders with the question both while it is pending and after it is answered.

The Interactions inbox rendering a question's media — images and links shown above the answer controls.
MediaItem — a kind, a url, and an optional caption:
kind—imagerenders inline;linkrenders as a labelled anchor.url— the source. Animageis an absolutehttpsURL or adata:image/*URI; alinkis an absolutehttp(s)URL. Remote images are https-only: the inbox content-security policy admitshttps:anddata:image sources but nothttp:, so anhttp:image would be a record that could never render — it is rejected up front, not silently dropped. Anchors are not governed by that policy, so anhttp:link stays valid.caption— optional accessibility text: an image’s alt text or a link’s display label.
Caps
Media is bounded by the contract. These are wire-contract properties — they bound the durable record and the stream frame it is replayed in — not operator settings:
The per-question total budget is load-bearing beyond the per-item cap: the pending
inbox backlog is replayed whole to every stream consumer on every (re)connect, so a
per-question ceiling bounds a bytes × pending × clients amplification the per-item
cap alone does not.
Limits
- Display-only. Media rides the question, never the answer. The person’s reply carries no media.
- Not forwarded to channels. A question always persists to the inbox, where its media renders. When the same question is also delivered to a channel, only the question text is forwarded — the media stays in the inbox.
- Privacy. A remote
httpsimage is fetched directly by the reader’s browser, which discloses the reader’s IP (and request timing) to the image host. An agent that must avoid that discloses nothing by using adata:image/*URI (the image travels inline in the record) or a same-origin URL the deployment serves itself. - Validated before anything is stored. Invalid media — an empty list, a
non-
http(s)link, a non-https/non-data:imageimage, an over-cap url, data URI, caption, or total, a blank caption, or more thanMEDIA_MAX_ITEMSitems — raises loudly when the question is built, before any state is written.
External questions
Anexternal question requires a link. A string link is a template carrying the
literal {callback_url} placeholder; a callable link receives the callback URL,
builds the external resource, and returns its final URL. The returned answer is the
payload the callback delivered — a JSON POST body, or the query params of a
GET confirm flow — validated against the question’s schema when one is declared.
ask_external packages this as a transformer extension:
it wraps a tool that builds an external resource and drives the whole flow. The
wrapped tool takes a callback_url parameter and returns the URL to visit.
Delivering to a channel
By default a pending question surfaces on the authenticated stream — the Studio inbox. Passingchannel="<name>" to ask_user hands delivery to a registered
channel plugin (Telegram, Slack, SMS/WhatsApp) instead: the question mints a
public callback ticket exactly like an external ask, the plugin pushes it to
the person on that medium, and the reply comes back through the same callback
door. The asking tool blocks exactly as before — only the delivery leg changes.
An unknown channel name, or a delivery failure, raises loudly before the run
would wait on an answer that can never arrive. See
Author a channel.
The doors
The interactions surface exposes four HTTP doors. Two are authenticated and two are public:
Publicness is an access-control configuration decision,
not a property of the route code.
Security in brief
- The ticket is the capability. It is a high-entropy bearer token that expires
on its TTL; single use is enforced by an answered-state guard, and a provider
retry against a live ticket gets an idempotent
200 already_answered. - GET never mutates. Link scanners prefetch URLs, so the GET door serves a byte-constant confirm page whose form POSTs back — the human’s click performs the claim.
- Uniform 404 and no reflected input. Unknown, expired, and pruned tickets return the same response; callback pages interpolate no request-derived value.
- Signature verification runs in core. Bind a webhook verifier to an external question and the POST door verifies the raw body before the answer is accepted — failing closed.
External questions require
INTERACTIONS_PUBLIC_BASE_URL (an https:// origin,
since the callback URL is a bearer capability) and Redis 7.0+. Callback payloads
arrive through an unauthenticated door, so every stream consumer must treat them
as untrusted.ask_user across the
answer formats and wires an ask_external callback end to end. See the
HTTP API reference for the interaction routes and the
CLI reference for tai interactions.

