Elicitation Forms¶
Overview¶
Elicitation forms let the agent shape an exchange to fit the moment: instead of a wall of prose, it renders an interactive form whenever a structured turn communicates better than text. A multi-part question becomes one form you answer with a click; a recommendation arrives as weighable cards; a disagreement is shown side-by-side so you can overrule it in one tap. The goal is plainly to improve human/AI communication — make the back-and-forth faster, clearer, and less ambiguous.
Every form is one declarative artifact — a FormSchema of
FormQuestions — built from plain data with form_from_dict, validated
once with collect_form_response, and rendered by surface-specific
renderers. The same artifact renders richly on a widget-capable client
and degrades gracefully everywhere else; the answer is validated the
same way regardless of surface.
On top of that shared substrate sits a small, growing communication grammar — a family of constructs, each a member of the one form model:
- intake — gather several independent decisions as one (multi-select-capable) form;
- decision — offer a recommended option with a rationale and per-option tradeoffs;
- pushback — disagree with a stated approach and offer a concrete alternative;
- progress — report a set of items by status and surface the blocked ones as a picker.
This feature owns the form model, the renderers, the validator, and the MCP tools that expose them. When a construct fires in a conversation is a judgment call governed by the agent's decision routine, not by this subsystem.
Concepts¶
One substrate, many constructs¶
A construct is not a parallel system — it is meaning and presentation
layered on the single form model. Adding a construct almost never adds a
new round-trip or a new validator; it adds a QuestionType and a few
optional FormQuestion fields, and reuses the existing answer path.
| Construct | QuestionType |
Answer | Extra fields |
|---|---|---|---|
| intake | single_select / multi_select / … |
the picked value(s) | — |
| decision | decision |
one option | rationale, option_notes, recommended |
| pushback | pushback |
one option | user_position (+ reuses recommended, rationale, option_notes) |
| progress | progress |
one blocked item | progress_items (+ reuses recommended, rationale) |
decision, pushback, and progress are all presentation-enriched
single-selects: the answer is one option, validated by membership
exactly like a plain single_select. What differs is the rendering and
the framing.
Validation is never skipped (R4)¶
collect_form_response is the one validator. A missing required field
with no default, or a value outside a select's options, raises
FormValidationError naming every offending field so the caller can
re-ask just those — it never silently accepts malformed input. This holds
for every construct, because every construct's answer is a select answer
underneath.
Surfaces, and graceful degradation¶
A form renders three ways, in order of richness:
- Widget (
form_to_widget_html→show_widget) — the rich surface: cards, badges, the three-bucket progress board, dissent framing. Renders on widget-capable clients (claude.ai / Cowork). Answers post back through a sentinel-marked JSON block which you validate withcollect_form_response. - AskUserQuestion (
form_to_askuserquestion) — the fallback: batched payloads (≤4 questions each), recommendation-first. The constructs collapse to a recommendation-ordered single-select here. Works in a plain terminal. - Native MCP elicitation (
form_to_elicitation_schema) — a JSON-schema form for clients with native elicitation. It does not render on Claude Code today and lacks multi-select, so it is not the default.
The terse reply vocabulary (y / go / 1) answers any construct on any
surface — a form never blocks a keyboard-only user.
The list render variant (list_style)¶
A single_select or multi_select can set list_style: "ordered"
(numbered) or "unordered" (bulleted) to render its options as a familiar
intro-sentence-plus-list shape — each item pickable by mouse or the
1 / 2 / 3 vocabulary — instead of a dropdown or checkboxes. This is
presentation only: the answer and its validation are unchanged. It is a
render option on the select types, not a separate construct.
Design & extension¶
The grammar's extension gate¶
A new construct is justified only when rendering or answer-meaning
genuinely differs. If the answer is shaped like an existing one (a
single-select pick), prefer reusing that answer path and adding a
QuestionType plus optional fields — not a parallel validator. If only
the look differs, it is a render variant of an existing type, not a new
member. The list_style list render is the worked example: it ships the
"intro + selectable list" shape as an option on the select types rather
than as a fifth construct.
Adding a construct — the touch points¶
Even a construct that reuses the single-select answer path must register
its QuestionType at each enumeration site: the model, the definition
validator, the widget submit-script reader, the native-schema mapper, the
MCP tool schema (and its count guard), and the driving skill's docs. A
per-type "rejects out-of-option" test is the cheap guard that catches a
missed validation site. Prove it with a non-mocked round-trip — render,
submit, collect — not just unit tests.