AEP-0006: Structured Input Requests (kind: "form")
Adds a fifth attention.requested kind, form, and one payload object, fields, that generalizes today's options (single closed-choice) to a flat, typed, multi-field form: multiple named fields, each a string, number, boolean, or single/multi-select enum, each independently capture-gated.
Draft: the AEP suite is pre-v0.1; each document's status lives in its header table. This page is rendered from the canonical spec/AEP-0006-structured-input-requests.md, which is the normative text.
| Field | Value |
|---|---|
| AEP | 0006 |
| Title | Structured Input Requests |
| Type | Standards Track — Core |
| Status | Accepted (2026-07-07: normative text, fixtures, and prototype landed in one change per GOVERNANCE.md §2) |
| Sponsor | AEP maintainers (per GOVERNANCE.md §2) |
| Created | 2026-07-07 |
| Requires | AEP-0001 (envelope, capture), AEP-0002 (taxonomy, attention lifecycle), AEP-0003 §9 (capture/redaction pipeline) |
| Supersedes / Superseded-by | n/a (additive to AEP-0002 §5.3 attention.requested) |
Abstract
Adds a fifth attention.requested kind, form, and one payload object,
fields, that generalizes today's options (single closed-choice) to a
flat, typed, multi-field form: multiple named fields, each a string,
number, boolean, or single/multi-select enum, each independently
capture-gated. attention.answered gains a matching values object.
No existing field, kind, or lifecycle state changes meaning; kind: "form" and
data.fields/data.values are new, optional, ignorable by any consumer that
predates this AEP. One vendor-neutral shape replaces what would otherwise be
N per-vendor special cases (Claude Code's AskUserQuestion, MCP's
elicitation/create, and whatever the next agent runtime calls the same
idea).
Motivation
The initial reference adapter mapping (the Claude Code adapter's
handleAskUserQuestion, live-verified against a real session) mapped Claude
Code's AskUserQuestion tool onto the existing kind: "input" +
options[{id,label}] shape by emitting one attention.requested per question
and holding an answer-barrier across all of them.
That adapter-only fix is complete and correct for what it covers (a batch of
independent single-select questions), but it is a workaround built on a payload
shape designed for permission prompts (options: [{id:"allow"},{id:"deny"}]),
not for general structured input. It exposed two real spec gaps rather than
adapter bugs:
options[].labelhas no content-capture exception. AEP-0002 §5.3 marksattention.requested.prompt[metadata*]: an explicit exception letting it carry model-generated text atredacted+ while degrading to a structural synthesis atmetadata(AEP-0003 §9.2).options[].labelhas no such exception; it is flatly[metadata], which is correct for"Allow"/"Deny"(structural, vendor-fixed strings) but wrong for"PostgreSQL"or"SQLite"(model-generated content, exactly likeprompt).Today's adapter works around this by treating option ids as 1-based indexes and gating labels through the same
redactedcheck asprompt: a reasonable patch, but it is inventing a convention the schema doesn't sanction.No protocol-level batching or field typing. "One
attention.requestedper question, barrier on the client" is an adapter-local convention, not something a consumer can discover or rely on. A question with a numeric range, a boolean toggle, or several simultaneous fields (a compact form, not N sequential prompts) has no expression at all: everything has to be flattened into single-select choice lists or free text.
Both gaps recur beyond Claude Code. MCP's elicitation/create
(requestedSchema, a JSON-Schema subset restricted to flat objects of
string/number/boolean/enum properties, 2025-06-18 spec) is the same
concept with the same restriction-to-flat-primitives philosophy, arrived at
independently.
This convergence is notable: CC PermissionRequest and MCP elicitation are
point-to-point (one consumer, no fan-out, no audit). AEP's attention lifecycle
already solves the fan-out/audit half of that sentence; this AEP closes the
"point-to-point" half by giving the content of a structured request a
vendor-neutral shape too, instead of every adapter reinventing it against a
payload built for yes/no prompts.
Fleet-observer test (AEP-0001 §3.2): does a fleet observer act on this?
Yes, identically to how it already acts on kind: "permission"/"input":
a human (or delegate) reads a question and answers it remotely. kind: "form" is not new behavior for a consumer, only a richer payload for
the same lifecycle events it already renders.
Specification
AEP-0002 §5.3, attention.requested: replace the kind enum and add fields
Current text (the two clauses being replaced):
kind: enum(permission|input|auth|review|other) REQ [metadata]
options?: [{ id: string, label: string }] [metadata](closed-choice answers, e.g. allow/deny)
New text:
kind: enum(permission|input|auth|review|form|other) REQ [metadata](formdenotes a structured multi-field request; seefieldsbelow),options?: [{ id: string [metadata], label: string [metadata*] }] > [metadata](closed-choice answers; gated by exception, same rule asprompt: at capturemetadataalabelMUST be a structural placeholder (e.g. the option's 1-based index as a string), never model-generated content; atredacted+ it may carry the real label through the AEP-0003 §9.4 pipeline),fields?: [field_spec] [metadata](present only when >kind: "form"; see below). Forkind: "form"the (already-REQ)promptcarries the form's overall message or title; its existing[metadata]exception rule applies to it unchanged.field_spec(new common payload object, joinstool_ref/error_info/usage` in §5.2):
field_spec = {
id: string REQ [metadata]
type: enum(string|number|boolean|select) REQ [metadata]
label: string REQ [metadata*] (same prompt/options exception: structural placeholder at metadata, real content at redacted+)
required: boolean [metadata] (default false)
multi: boolean [metadata] (default false; only meaningful when type:"select": multiple choices may be selected)
other: boolean [metadata] (default false; type:"select" only: the responder MAY answer with free text instead of a choices[].id; see the values gating rule below)
choices: [{ id: string [metadata], label: string [metadata*] }] [metadata] (REQUIRED when type:"select"; same label exception as above)
min: number [metadata] (type:"number" only)
max: number [metadata] (type:"number" only)
}This is deliberately the same four-primitive restriction MCP elicitation
imposes on requestedSchema (string/number/boolean/enum, flat, no nesting,
no arrays-of-objects). That is not coincidence: it matches the
constrained-subset philosophy schemas/codegen/generate.py already enforces
(its header comment: "objects of string/integer/boolean/enum/nested-object/array
members, plus primitive union type lists. Anything else fails loudly").
One honest caveat: the generator today maps only integer, not JSON Schema's
number (generate.py lines 65/135/204); field_spec keeps number for MCP
parity, so the prototype change MUST include the small generator extension, per
the generator's own header rule ("extend the generator in the same change that
extends the schema subset").
select with multi: true covers AskUserQuestion's multiSelect; select
with multi: false (default) covers its single-select questions and today's
permission options as a degenerate one-field case; other: true
covers AskUserQuestion's always-present custom-answer affordance: the
path live verification actually exercised (custom free-text answers a closed
select cannot express). This is a deliberate, evidence-driven divergence
from MCP elicitation's closed enum (see Rationale).
AEP-0002 §5.3, attention.answered: add values
Current text:
answer: { option?: string [metadata], text?: string [redacted] } REQ
New text:
answer: { option?: string [metadata], text?: string [redacted], > values?: object [metadata*] } REQ
valuesis present only answering akind: "form"request. It is a flat map offield_spec.idto the field's answer: a string fortype:"string", a number fortype:"number", a boolean fortype:"boolean", achoices[].idstring for single-select, an array ofchoices[].idstrings formulti:true. For aselectfield withother: true, a free-text string that is not achoices[].idis also allowed.Gated by exception: a
string-typed field's value (and anotherfree-text value on aselectfield) is user-authored text and follows the same rule asanswer.text:redactedminimum, nevermetadata;number/boolean/closed single- or multi-selectvalues are structural (ids/primitives, never model- or user-authored prose) and stay[metadata].An emitter MUST reject (
control.rejected{reason:"invalid"}) acontrol.attention.respondwhosevaluesdoes not cover everyfield_specwithrequired: true, or that carries a non-choices[].idstring for aselectfield whoseotheris absent/false.
AEP-0002 §7, the attention lifecycle: no change
kind: "form" uses the identical requested → [routed]* → answered → resolved | timeout lifecycle (AEP-0002 §7 rules 1 to 4) with no new states.
One attention.requested(kind="form") carries an entire multi-field batch
as a single event; this AEP does not require or recommend the
one-event-per-field pattern the initial adapter mapping used for
AskUserQuestion's per-question cards; see Rationale for why the batch shape
is preferred going forward, and Backward compatibility for what happens to
that initial adapter.
AEP-0002 §6, registry: no closed-set change needed
kind values are payload-internal enums (AEP-0002 §5.3), not registry-level
types.json entries; adding form to the enum is the additive,
minor-version change AEP-0002 §6 already describes for "adding an optional
core type," applied here to an enum member rather than a whole type: no
new row in schemas/registry/types.json.
schemas/types/attention.requested.schema.json / attention.answered.schema.json: updates
- Add
"form"to thekindenum. - Add
fields(array offield_specobjects,x-aep-capture: metadata,label/choices[].labelmarked with the same$commentexception patternpromptalready uses). - Add
field_specasschemas/registry/common object or inline$defsin the two schema files (codegen-generator decision, not normative; either works against the existing generator subset). attention.answered.schema.json: addvalues(type: object,x-aep-capture: metadata, per-key gating documented in a$commentsince JSON Schema cannot express "gate depends on the referenced field_spec.type" structurally; this is a normative prose rule enforced by emitters/aep validate, not a schema-mechanical one).
Conformance fixtures
Landed under conformance/fixtures/ in the same change that moved this AEP
to Accepted (GOVERNANCE.md §2, SEP rule):
golden/golden.jsonl: oneattention.requested(kind="form")line with a 4-field form (onestring, oneboolean, one closedselectwithmulti:true, oneselectwithother:true) and its matchingattention.answered{answer:{values:{...}}}(theotherfield answered with free text) +attention.resolved{resolution:"answered"}pair (sessions_form).Both runners schema-validate every golden line;
run.pyadditionally asserts the values rules and capture honesty on the pair, andaep validate(whichrun.jsdrives) applies the same cross-event checks to any JSONL stream.capture-gating/cases.json: the new category, with runner support in bothrun.pyandrun.jslanded in this same change: six cases proving astring-type (andotherfree-text)valuesentry is dropped atcapture: metadataand present atredacted+, while closed-select/number/booleanentries survive atmetadata; the asymmetric gating is the point of this AEP.run.jsexercises the shared implementation the reference adapter itself uses (gateFormValues);run.pyholds an independent implementation written from this spec text. (The pre-existingfixtures/redaction/cases.jsonremains the AEP-0003 §9.4 text-pipeline adversarial corpus: a different layer, untouched.)invalid/cases.json: akind="form"event whoseselectfield omitschoices(schema-mechanical: the field_spec items schema carries anif/thenconditional), and a two-event stream whoseattention.answeredvaluescarries a non-choices[].idstring for aselectfield withoutother:true(the cross-event prose rule, enforced independently byaep validateandrun.py). The missing-required-field rejection is emitter behavior, exercised in the adapter smoke (case 6), not a schema-validity fixture.mappings/claude-code.json: the first fixture underconformance/fixtures/mappings/, the directory AEP-0002 Annex A anticipates ("as they gain fixtures"), for the Claude Code table:AskUserQuestion → kind="form", one event per call, replacing the initial per-questionkind="input"cards, pinned at both theredacted+ andmetadataceilings so the label exception is machine-checked.run.jspins the reference adapter's own mapping module;run.py, which has no adapter implementation by design, checks each pinned payload's schema validity and required wire facts.
Both conformance runners (conformance/run.py, conformance/run.js) pass
every fixture above, including the two new categories.
Prototype
Landed with this AEP's acceptance (GOVERNANCE.md §2): the reference Claude
Code adapter's handleAskUserQuestion emits one
kind="form" event per AskUserQuestion call (the payload mapping is a
pure module, pinned by
the mappings fixture), validates answer.values per §5.3's MUST-reject rule
(an invalid answer is nacked control.rejected{reason:"invalid"} and the
request stays pending), gates emitted values type-dependently through
gateFormValues, and returns {behavior:"allow", updatedInput:{questions, answers}} with values mapped back to Claude Code answers by field id.
A reference consumer UI renders fields as an actual
multi-field form (text/number input, checkbox, single- or multi-select,
select-with-other per field_spec.type) with one submit that sends
answer.values through the existing respond path (its answer
type gained values).
The adapter's earlier attention-loop smoke cases were evolved, not replaced: every behavior the earlier mapping proved is re-proved at the form level (content fidelity, the free-text "Other" path, the required-field nack, timeout fail-open, non-gating-surface dismissal), written first and watched fail against the earlier adapter before the swap.
Rationale
Why one form event per call, not one attention.requested per
question (reversing the initial adapter mapping's own pattern)? That
mapping chose
per-question events because it had to fit inside the existing kind: "input" shape, which has no concept of "this event is one of a batch."
That forced an adapter-local answer-barrier (a call closure tracking
remaining) that a generic consumer cannot discover from the wire: it just
seems like several unrelated attention.requested events that happen to
race.
A single form event with a fields array makes the batch a wire fact: any
consumer, not just one that knows Claude Code's tool-call internals, can render
"3 fields, 1 form, 1 answer" correctly.
This also removes the failure mode the initial adapter mapping had to
special-case (partial answers on timeout, §5.3's "answered question stays
resolved, unanswered gets timeout" split): a form's fields either all arrive in
one values map or the whole form times out, which is simpler for every
consumer to reason about, at the cost of finer-grained partial-progress
visibility. That trade is deliberately made here, not smuggled in.
Why restrict field_spec to four primitive types with no nesting,
when JSON Schema (and MCP's requestedSchema) could in principle allow
more? Two independent arguments converge: (1) MCP elicitation's own spec
text ("intentionally not supported to simplify client implementation")
made the identical call for the identical reason; (2) AEP's schemas/ codegen/generate.py already only supports this exact subset ("Anything
else fails loudly: extend the generator in the same change that extends
the schema subset").
Choosing anything richer would mean extending codegen and asking every future
consumer to implement a bigger form language, for a case (rich nested input)
attention requests are not actually used for today. This mirrors the design
argument behind attr-match ("deliberately not a language", AEP-0003 §6): the
same discipline applied to form fields.
Why add other: true when MCP elicitation's enum is closed? Because
the evidence demands it: live verification answered through the custom-text
path, and
AskUserQuestion's UI offers "Other" on every question. A closed select
would make the most-used observed behavior inexpressible and regress the
exact scenario that motivated this AEP. This is the one deliberate
divergence from the MCP cross-check, and it is opt-in per field (default
false keeps the MCP-congruent closed shape).
Why not adopt ACP's four-way PermissionOptionKind
(allow_once/allow_always/reject_once/reject_always)? It is a real,
useful idea, but it is orthogonal to this AEP: it extends kind: "permission"'s options, not the general form case, and deserves its own
narrower proposal if a real adapter need surfaces (no current adapter asks
for "always allow" semantics; AEP-0004's control profile doesn't yet have a
concept of a standing decision policy to attach it to). Scoping it in here
would couple two unrelated design questions.
Why generalize now instead of waiting for a second real vendor need
(the general caution against over-designing against one
UI)? MCP elicitation is the second vendor need: it exists today,
independently, with the same shape. This AEP is designed against MCP's
requestedSchema/ElicitResult (four primitive types, flat, three-action
response) as the primary cross-check, with AskUserQuestion as the
motivating case, precisely so it does not calcify around Claude Code's
specific 1-4-questions/2-4-options/12-char-header UI. Section "Backward
compatibility" below shows the MCP mapping explicitly for this reason.
Backward compatibility
Additive within AEP-0002's evolution bias (AEP-0001 §11): a new kind enum
member and two new optional payload fields (fields, values). Every
existing attention.requested(kind="permission"|"input"|"auth"|"review"| "other") event and its schema stay byte-for-byte valid. Two distinct
consumer populations, stated honestly rather than blurred:
- Render-level consumers (Mission Control, sinks, routers) that read
kindas a string degrade gracefully: an unknown"form"renders as its REQpromptplus a free-text reply box, less richly but never incorrectly. - Schema-pinned validators (
aep validateagainst a published 0.1 schema set, strict enum) would reject akind:"form"event, because thekindenum is closed. Two timelines, one rule:- Accepted before v0.1 tags (the repo is pre-release; every spec is Draft): the change folds into the v0.1 Draft directly. AEP-0002 §5.3 text and the 0.1 schemas are updated in place, there is no published consumer to protect, and no version dance occurs. This is the cheaper path and the recommended one if this AEP lands during the current pre-release window.
- Accepted after v0.1 ships: the change rides a minor version (0.2
schemas) (the same additive/minor mechanics AEP-0002 §6 defines for
adding an optional core type, applied to an enum member), and emitters
MUST NOT emit
kind:"form"while declaringaep: "0.1".
Effect on the initial adapter code specifically: handleAskUserQuestion
(shipped and live-verified before this AEP existed) was not wrong. It was a
correct, complete mapping onto the pre-AEP-0006 payload shape, and it stayed
exactly as deployed until this AEP was Accepted with its prototype (the same
change).
The per-question kind:"input" events became one
kind:"form" event; this was a within-adapter migration (swap the
emission shape in handleAskUserQuestion, keep the surrounding hook
plumbing, timeout timer, and dismissal logic, which already operated at the
call level via the call closure; the code was closer to the
form shape than to independent events, since call.remaining/
call.answers already tracked the whole batch, not each question
separately).
The underlying attention-loop mechanics (control round-trip,
updatedInput suppressing CC's own dialog, free-text "Other") are
unchanged: this AEP changes the payload's shape, not the loop.
Effect on consuming UIs: a consumer's existing option-reading and
button-rendering paths for kind: "permission"/"input" cards keep working
unchanged. Rendering kind: "form" is new, additive UI work: a
consumer does not need to change to stay correct; it changes only to render
the new kind well, landing together with the adapter half so question
cards never degrade during a rollout.
Effect on the Codex adapter: none. Codex exposes no clarifying-question
hook today (confirmed against its current documentation and a live pass);
this AEP's mapping is ready the day one appears, with MCP
elicitation as the second concrete adapter target in the meantime (Claude
Code, as an MCP client, could in principle surface elicitation through
the same kind: "form" shape, an available but out-of-scope-for-now
extension noted under Open questions).
Security considerations
Capture/redaction (AEP-0003 §9). This is the section this AEP changes
the most: today's flat options[].label: [metadata] silently permits
content leakage risk in the other direction: because there is no
exception clause, a strict reading requires vendor-fixed strings only, so
the initial adapter's practice of gating labels through redacted
(matching prompt's existing exception) was following the spirit of
AEP-0003 §9 while technically extending an un-excepted field.
This AEP closes that gap by giving field_spec.label/choices[].label the
same explicit [metadata*] exception prompt already has, and by making the
values gating rule type-dependent rather than uniform: a string field is
free text (an operator's or user's own words, redacted minimum, mirroring
answer.text), while number/boolean/select values are structural
identifiers with no more content-leakage risk than answer.option today.
Getting this asymmetry wrong in either direction is a real defect class:
over-gating breaks legitimate structural data at metadata ceilings,
under-gating leaks free-text form answers at a ceiling meant to exclude them (a
high-severity shape observed where free text was silently dropped in the other
direction; here the risk is silently retained above ceiling). The redaction
fixture required above exists specifically to make this asymmetry a
conformance-tested invariant, not adapter-author discipline.
The same invariant holds at fan-out hops
(AEP-0001 §8.2): a hop rewriting capture below redacted has no source
context to classify values entries or synthesize labels, so it degrades
the [metadata*] fields structurally (prompt synthesis, labels → ids,
values → structural entries only). This AEP's acceptance review found
and fixed a reference implementation that retained that content across a
hop; the corpus that pins it is conformance/fixtures/downlevel/.
Control authorization (AEP-0004). No change: control.attention.respond
carries answer today and answer.values under this AEP identically:
the same subject/cause correlation and control.accepted/
control.rejected ack rules apply unchanged. The new "reject if a required
field is unanswered" rule (§ above) is a new control.rejected{reason: "invalid"} trigger, not a new rejection reason or authorization
concept.
Identity/bridges (AEP-0005). No change to bridge annexes: fields/
values are ordinary data payload content and cross the CE/OTLP bridges
under the existing content-mapping rules for attention.* types.
Open questions
- Should Claude Code's own MCP-client role surface
elicitation/createthrough this samekind: "form"mapping? Natural and probably desirable (it is literally the cross-check this AEP was designed against), but out of scope for the reference prototype, which targetsAskUserQuestiononly. Revisit once a design partner actually runs CC-as-MCP-client through the adapter (no evidence either way yet): post-v0.1, alongside themessage/streamsub-profile question, since both concern MCP-adjacent surfaces not yet exercised end-to-end. - Should
field_specsupport apattern(regex) constraint fortype:"string", mirroring MCP'sformat: email|uri|date|date-time? Deferred: no adapter need observed yet, and every addition tofield_specis another thing every consumer must at least tolerate. Pull forward only if a real elicitation/AskUserQuestion case needs validated input, not free text. - ACP's
allow_once/allow_always/reject_once/reject_alwaysoption-kind idea (Rationale, above) is explicitly parked as a separate, future, narrower proposal againstkind: "permission", not folded into this one. Owner: whoever picks it up; no milestone, no current adapter pressure.