Skip to content

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_htmlshow_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 with collect_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.